diff --git a/backup/TreasureListener_backup_20260607_002531.java b/backup/TreasureListener_backup_20260607_002531.java new file mode 100644 index 0000000..f0c5443 --- /dev/null +++ b/backup/TreasureListener_backup_20260607_002531.java @@ -0,0 +1,784 @@ +package Sdf1_game; + +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.List; +import java.util.Map; + +public class TreasureListener implements Listener { + + private final Main plugin; + private final TreasureManager manager; + + public TreasureListener(Main plugin, + TreasureManager manager) { + this.plugin = plugin; + this.manager = manager; + } + // ★ 玩家攻击玩家时计数 + @org.bukkit.event.EventHandler + public void onDamage( + org.bukkit.event.entity + .EntityDamageByEntityEvent e) { + + if (!(e.getDamager() + instanceof org.bukkit.entity.Player)) + return; + if (!(e.getEntity() + instanceof org.bukkit.entity.Player)) + return; + + org.bukkit.entity.Player attacker = + (org.bukkit.entity.Player) + e.getDamager(); + ItemStack hand = + attacker.getInventory() + .getItemInMainHand(); + + if (hand == null) return; + if (!TreasureInventory + .isCustomItem(hand)) return; + + String[] mark = + TreasureInventory.parseMark(hand); + if (mark == null) return; + + String owner = mark[0]; + String region = mark[1]; + String rName = mark[2]; + int maxUse = + Integer.parseInt(mark[3]); + int useCount = + Integer.parseInt(mark[4]) + 1; + + // ★ 只有攻击上限>0的物品才计数 + if (maxUse <= 0) return; + + // ★ 检查归属 + if (!owner.equalsIgnoreCase( + attacker.getName())) return; + + plugin.getLogger().info( + "[寻宝] ★攻击: " + + attacker.getName() + + " " + rName + + " " + useCount + + "/" + maxUse); + + // ★ 更新lore里的次数 + ItemMeta meta = hand.getItemMeta(); + if (meta != null && meta.hasLore()) { + List lore = meta.lore(); + for (int i = 0; + i < lore.size(); i++) { + String line = + net.kyori.adventure.text + .serializer.legacy + .LegacyComponentSerializer + .legacySection() + .serialize(lore.get(i)); + if (line.contains("§0§kCUSTOM")) { + String newMark = + "§0§kCUSTOM|" + + owner + "|" + + region + "|" + + rName + "|" + + maxUse + "|" + + useCount; + lore.set(i, + Component.text(newMark)); + break; + } + } + meta.lore(lore); + hand.setItemMeta(meta); + } + + // ★ 打印给玩家看 + attacker.sendMessage( + "§e[寻宝] " + rName + + " 攻击次数: " + + useCount + "/" + maxUse); + + // 达到上限 → 回收 + if (useCount >= maxUse) { + attacker.getInventory() + .setItemInMainHand(null); + plugin.getTreasureManager() + .removeClaim(owner, rName, + region); + attacker.sendMessage( + "§c[寻宝] " + rName + + " 攻击次数用完,已回收"); + attacker.getWorld().dropItemNaturally( + attacker.getLocation(), + new ItemStack(Material.AIR)); + } + } + + + // ★ 丢弃拦截 + @org.bukkit.event.EventHandler + public void onDrop( + org.bukkit.event.player + .PlayerDropItemEvent e) { + if (TreasureInventory.isCustomItem( + e.getItemDrop().getItemStack())) { + e.setCancelled(true); + e.getPlayer().sendMessage( + "§c[寻宝] 寻宝物品不可丢弃"); + } + } + // ★ 死亡清除 + @org.bukkit.event.EventHandler + public void onDeath( + org.bukkit.event.entity + .PlayerDeathEvent e) { + org.bukkit.entity.Player player = + e.getEntity(); + for (int i = 0; + i < player.getInventory() + .getSize(); i++) { + ItemStack item = + player.getInventory() + .getItem(i); + if (item == null) continue; + if (!TreasureInventory + .isCustomItem(item)) continue; + e.getDrops().remove(item); + player.getInventory() + .setItem(i, null); + } + } + + // ★ 物品掉落到地上时检测(丢弃后立即清除) + @org.bukkit.event.EventHandler + public void onItemSpawn( + org.bukkit.event.entity + .ItemSpawnEvent e) { + ItemStack item = + e.getEntity().getItemStack(); + if (!TreasureInventory + .isCustomItem(item)) return; + + // ★ 延迟1秒后检查,如果还在地上就销毁 + // (已捡起的不会被销毁) + final org.bukkit.Location loc = + e.getLocation(); + final String itemKey = + item.getType().name() + + "_" + item.getAmount(); + org.bukkit.Bukkit.getScheduler() + .runTaskLater(plugin, () -> { + for (org.bukkit.entity.Entity ent : + loc.getChunk() + .getEntities()) { + if (ent instanceof + org.bukkit.entity.Item) { + org.bukkit.entity.Item di = + (org.bukkit.entity.Item) + ent; + if (TreasureInventory + .isCustomItem( + di.getItemStack())) { + String[] mark = + TreasureInventory + .parseMark( + di.getItemStack()); + if (mark != null) { + // 清除领取记录 + plugin + .getTreasureManager() + .removeClaim( + mark[0], + mark[2], + mark[1]); + } + di.remove(); + break; + } + } + } + }, 20L); + } + + // ===== 圈地工具选点 ===== + + @EventHandler + public void onToolClick(PlayerInteractEvent e) { + Player p = e.getPlayer(); + + // 只处理主手 + if (e.getHand() != EquipmentSlot.HAND) + return; + + if (!TreasureRegion.isTool( + p.getInventory().getItemInMainHand())) + return; + + Block b = e.getClickedBlock(); + if (b == null) return; + + e.setCancelled(true); + Location loc = b.getLocation(); + TreasureRegion tr = + TreasureRegion.get(p.getName()); + + if (e.getAction() == Action.LEFT_CLICK_BLOCK) { + // A点 + tr.setPos1(loc); + int ex = tr.getExpand(); + p.sendMessage("§a§l[圈地] §7A点已设置: §e" + + loc.getBlockX() + "," + + loc.getBlockY() + "," + + loc.getBlockZ()); + p.sendMessage("§7扩展范围: §e" + + ex + "§7格"); + p.sendMessage("§7输入 §e/寻宝 expand <数字> " + + "§7调整范围"); + p.sendMessage("§7输入 §e/寻宝 区域名 " + + "§7命名并保存"); + + } else if (e.getAction() + == Action.RIGHT_CLICK_BLOCK) { + tr.setPos2(loc); + p.sendMessage("§a§l[圈地] §7B点已设置: §e" + + loc.getBlockX() + "," + + loc.getBlockY() + "," + + loc.getBlockZ()); + // ★ 设置B点后自动显示边框 + tr.showBorder(); + showPreview(p, tr); + } + } + + // ===== 右键寻宝宝箱 ===== + + @EventHandler + public void onChestInteract( + PlayerInteractEvent e) { + if (e.getAction() != Action.RIGHT_CLICK_BLOCK) + return; + if (e.getHand() != EquipmentSlot.HAND) return; + + Block b = e.getClickedBlock(); + if (b == null || b.getType() != Material.CHEST) + return; + Location loc = b.getLocation(); + if (!manager.isTreasure(loc)) return; + + Player p = e.getPlayer(); + e.setCancelled(true); + + String regionName = null; + for (Map.Entry en + : manager.getActiveChests().entrySet()) { + if (en.getKey().equals(loc)) { + regionName = en.getValue(); + break; + } + } + + b.setType(Material.AIR); + manager.removeChest(loc); + + if (regionName != null) { + TreasureConfig tc = + manager.getConfigs().get(regionName); + if (tc != null) { + TreasureInventory.openTreasure( + p, tc, loc); + + + } + } + } + // ★ 玩家下线时标记待回收 + @org.bukkit.event.EventHandler + public void onQuit( + org.bukkit.event.player.PlayerQuitEvent e) { + String name = e.getPlayer().getName(); + // 标记该玩家有物品需要在上线时回收 + plugin.getTreasureManager() + .getPendingReclaims().add(name); + } + + // ★ 寻宝GUI点击处理 + @org.bukkit.event.EventHandler(priority = + org.bukkit.event.EventPriority.HIGHEST, + ignoreCancelled = true) + public void onTreasureGUI( + org.bukkit.event.inventory + .InventoryClickEvent e) { + String title = e.getView().getTitle(); + if (!title.contains("寻宝结果")) return; + + int slot = e.getRawSlot(); + if (slot < 0) return; + e.setCancelled(true); + if (e.getInventory().getSize() != 54) return; + + org.bukkit.entity.Player player = + (org.bukkit.entity.Player) + e.getWhoClicked(); + Main plugin = (Main) org.bukkit.Bukkit + .getPluginManager() + .getPlugin("Sdf1_game"); + if (plugin == null) return; + + TreasureManager tm = + plugin.getTreasureManager(); + TreasureData td = + tm.getPlayerTreasureData( + player.getName()); + if (td == null) return; + + // ★ 刷新按钮(49格) + if (slot == 49 && !td.isRefreshed) { + int cost = td.config.refreshBondCost; + BondBridge bb = + plugin.getBondBridge(); + int current = 0; + if (bb != null && bb.isHooked()) { + current = bb.getBonds( + player.getName()); + } + if (current < cost) { + player.sendMessage( + "§c[寻宝] 债券不足,需要" + + cost + + "张,当前" + + current + "张"); + return; + } + if (bb != null && bb.isHooked()) { + bb.addBonds(player.getName(), + -cost, "寻宝刷新"); + } + tm.markRefreshed(td.chestLoc); + player.closeInventory(); + TreasureInventory.openRefreshed( + player, td.config, + td.chestLoc); + player.sendMessage( + "§e[寻宝] 已刷新!消耗" + + cost + "债券"); + return; + } + + // ★ 确认领取按钮(50格) + // 确认领取(50格) + if (slot == 50) { + player.closeInventory(); + // ★ 只调用一次 + TreasureInventory.giveRewards( + player, td.config, + td.bondAmount, td.rolled); + tm.clearPlayerTreasureData( + player.getName()); + return; + } + + // ★ 其他槽位全部取消(只展示) + } + + // ===== 阻止破坏宝箱 ===== + + @EventHandler + public void onBreak(BlockBreakEvent e) { + if (e.getBlock().getType() != Material.CHEST) + return; + if (manager.isTreasure( + e.getBlock().getLocation())) { + e.setCancelled(true); + e.getPlayer().sendMessage( + "§c宝箱不能破坏"); + } + } + + // ===== GUI点击 ===== + + @EventHandler + public void onInventoryClick( + InventoryClickEvent e) { + if (e.getInventory().getHolder() + instanceof TreasureInventory.GuiHolder) { + TreasureInventory.handleClick( + e, plugin.getBondBridge(), plugin); + } + } + + @org.bukkit.event.EventHandler(priority = + org.bukkit.event.EventPriority.HIGHEST) + public void onInvClick( + org.bukkit.event.inventory + .InventoryClickEvent e) { + if (e.getWhoClicked() == null) return; + org.bukkit.entity.Player p = + (org.bukkit.entity.Player) + e.getWhoClicked(); + Main plugin = (Main) Bukkit + .getPluginManager() + .getPlugin("Sdf1_game"); + String title = e.getView().getTitle(); + + // ========== 1. 寻宝GUI ========== + if (title.contains("寻宝结果")) { + e.setCancelled(true); + int slot = e.getRawSlot(); + if (slot < 0) return; + if (e.getInventory().getSize() != 54) + return; + handleTreasureGUI(p, slot, e); + return; + } + + // ========== 2. 检测自定义物品 ========== + ItemStack checkItem = null; + + // 光标有物品(普通点击) + if (e.getCursor() != null + && e.getCursor().getType() + != Material.AIR) { + checkItem = e.getCursor(); + } + + // shift-click + if (checkItem == null + && e.isShiftClick() + && e.getCurrentItem() != null + && e.getCurrentItem().getType() + != Material.AIR) { + checkItem = e.getCurrentItem(); + } + + // ========== 3. 非自定义物品直接放行 ========== + if (checkItem == null) return; + if (!TreasureInventory + .isCustomItem(checkItem)) return; + + // ========== 4. 确认是容器 ========== + InventoryType topType = e.getView() + .getTopInventory().getType(); + if (topType == InventoryType.PLAYER + || topType == InventoryType.CRAFTING + || topType == InventoryType.CREATIVE) { + return; + } + + // ========== 5. 自定义物品+容器 → 拦截 ========== + e.setCancelled(true); + + plugin.getLogger().info( + "[寻宝] ★★★拦截: " + p.getName() + + " 物品=" + checkItem.getType() + + " 容器=" + topType); + + // 非本人→直接没收 + String[] mark = TreasureInventory + .parseMark(checkItem); + if (mark != null + && !mark[0].equalsIgnoreCase( + p.getName())) { + checkItem.setAmount(0); + p.getOpenInventory().setCursor(null); + p.sendMessage( + "§c[寻宝] 该物品不属于你,已没收"); + return; + } + + // ★ 事不过三 + boolean confiscate = plugin + .getTreasureManager() + .onContainerBlock(p.getName()); + + if (confiscate) { + // 第3次:没收 + String itemName = mark != null + ? mark[2] : "?"; + String region = mark != null + ? mark[1] : "?"; + + checkItem.setAmount(0); + p.getOpenInventory().setCursor(null); + + plugin.getTreasureManager() + .unclaim(p.getName(), + itemName, region); + + p.sendMessage( + "§c[寻宝] 多次违规," + + itemName + " 已被没收!"); + } else { + // 第1-2次:退回 + if (p.getOpenInventory() + .getCursor() != null + && p.getOpenInventory() + .getCursor().getType() + != Material.AIR) { + ItemStack cur = p.getOpenInventory() + .getCursor(); + p.getOpenInventory().setCursor(null); + p.getInventory().addItem(cur); + } + int count = plugin + .getTreasureManager() + .getContainerBlockCount( + p.getName()); + p.sendMessage( + "§c[寻宝] 不可放入容器!" + + "第" + count + "次警告(" + + (3 - count) + + "次后没收)"); + } + } + + // ★ 寻宝GUI处理 + private void handleTreasureGUI( + org.bukkit.entity.Player player, + int slot, + org.bukkit.event.inventory + .InventoryClickEvent e) { + Main plugin = (Main) org.bukkit.Bukkit + .getPluginManager() + .getPlugin("Sdf1_game"); + if (plugin == null) return; + TreasureManager tm = + plugin.getTreasureManager(); + TreasureData td = + tm.getPlayerTreasureData( + player.getName()); + if (td == null) return; + + // 刷新按钮(49格) + if (slot == 49 && !td.isRefreshed) { + int cost = td.config.refreshBondCost; + BondBridge bb = + plugin.getBondBridge(); + int current = 0; + if (bb != null && bb.isHooked()) { + current = bb.getBonds( + player.getName()); + } + if (current < cost) { + player.sendMessage( + "§c[寻宝] 债券不足,需要" + + cost + "张"); + return; + } + if (bb != null && bb.isHooked()) { + bb.addBonds(player.getName(), + -cost, "寻宝刷新"); + } + tm.markRefreshed(td.chestLoc); + player.closeInventory(); + TreasureInventory.openRefreshed( + player, td.config, + td.chestLoc); + player.sendMessage( + "§e[寻宝] 已刷新!消耗" + + cost + "债券"); + return; + } + + // 确认领取(50格) + if (slot == 50) { + player.closeInventory(); + TreasureInventory.giveRewards( + player, td.config, + td.bondAmount, td.rolled); + tm.clearPlayerTreasureData( + player.getName()); + return; + } + } + + // ★ 拖拽拦截 + @org.bukkit.event.EventHandler(priority = + org.bukkit.event.EventPriority.HIGHEST) + public void onDrag( + org.bukkit.event.inventory + .InventoryDragEvent e) { + String title = e.getView().getTitle(); + + // ★ 垃圾站拖拽拦截 + if (title.contains("垃圾回收站")) { + for (ItemStack item : + e.getNewItems().values()) { + if (item != null + && TreasureInventory + .isCustomItem(item)) { + e.setCancelled(true); + Main plugin = (Main) Bukkit + .getPluginManager() + .getPlugin("Sdf1_game"); + if (plugin != null) { + plugin.getTreasureManager() + .onContainerBlock( + e.getWhoClicked() + .getName()); + } + e.getWhoClicked().sendMessage( + "§c[寻宝] 不可拖入垃圾站"); + return; + } + } + return; + } + + // ★ 通用容器拖拽拦截 + InventoryType type = e.getView() + .getTopInventory().getType(); + if (type != InventoryType.PLAYER + && type != InventoryType.CRAFTING + && type != InventoryType.CREATIVE) { + for (ItemStack item : + e.getNewItems().values()) { + if (item != null + && TreasureInventory + .isCustomItem(item)) { + e.setCancelled(true); + e.getWhoClicked().sendMessage( + "§c[寻宝] 不可放入容器"); + return; + } + } + } + } + + + // ★ 铁砧拦截 + @org.bukkit.event.EventHandler(priority = + org.bukkit.event.EventPriority.HIGHEST) + public void onAnvil( + org.bukkit.event.inventory + .PrepareAnvilEvent e) { + ItemStack first = + e.getInventory().getItem(0); + if (first != null + && TreasureInventory + .isCustomItem(first)) { + e.setResult(null); + } + } + + + // ★ 右键容器拦截 + @org.bukkit.event.EventHandler(priority = + org.bukkit.event.EventPriority.HIGHEST) + public void onInteract( + org.bukkit.event.player + .PlayerInteractEvent e) { + if (e.getItem() == null) return; + if (!TreasureInventory + .isCustomItem(e.getItem())) return; + if (!e.getAction().name() + .contains("RIGHT_CLICK")) return; + if (e.getClickedBlock() == null) return; + String bt = e.getClickedBlock() + .getType().name(); + if (bt.contains("SHULKER") + || bt.contains("CHEST") + || bt.equals("BARREL") + || bt.equals("ENDER_CHEST") + || bt.equals("HOPPER") + || bt.equals("DROPPER") + || bt.equals("DISPENSER")) { + e.setCancelled(true); + e.getPlayer().sendMessage( + "§c[寻宝] 不可放入容器"); + } + } + // ★ 上线时扫描背包回收 + @org.bukkit.event.EventHandler + public void onJoin( + org.bukkit.event.player + .PlayerJoinEvent e) { + plugin.getTreasureManager() + .reclaimPlayerItems( + e.getPlayer().getName()); + } + + // ★ 手持寻宝物品切换槽位时检查归属 + @org.bukkit.event.EventHandler + public void onHeld( + org.bukkit.event.player + .PlayerItemHeldEvent e) { + ItemStack item = + e.getPlayer().getInventory() + .getItem( + e.getNewSlot()); + if (item == null) return; + if (!TreasureInventory + .isCustomItem(item)) return; + + String[] mark = + TreasureInventory.parseMark(item); + if (mark == null) return; + + if (!mark[0].equalsIgnoreCase( + e.getPlayer().getName())) { + // 不是本人的→立即回收 + e.getPlayer().getInventory() + .setItem(e.getNewSlot(), null); + plugin.getTreasureManager() + .removeClaim(mark[0], mark[2], + mark[1]); + e.getPlayer().sendMessage( + "§c[寻宝] " + mark[2] + + " 不属于你,已回收"); + } + } + + // ===== 预览区域范围 ===== + + private void showPreview(Player p, + TreasureRegion tr) { + Location a = tr.getPos1(); + Location b = tr.getPos2(); + if (a == null || b == null) { + p.sendMessage("§c请先设置A点"); + return; + } + + int ex = tr.getExpand(); + int minX = Math.min(a.getBlockX(), + b.getBlockX()) - ex; + int maxX = Math.max(a.getBlockX(), + b.getBlockX()) + ex; + int minZ = Math.min(a.getBlockZ(), + b.getBlockZ()) - ex; + int maxZ = Math.max(a.getBlockZ(), + b.getBlockZ()) + ex; + + p.sendMessage("§e§l[圈地] 区域预览:"); + p.sendMessage("§7世界: §e" + + a.getWorld().getName()); + p.sendMessage("§7范围: §e[" + + minX + "," + minZ + "] ~ [" + + maxX + "," + maxZ + "]"); + p.sendMessage("§7大小: §e" + + (maxX - minX + 1) + " x " + + (maxZ - minZ + 1)); + p.sendMessage("§7扩展: §e" + ex + "格"); + p.sendMessage("§7输入 §e/寻宝 expand <数字> " + + "§7调整"); + p.sendMessage("§7输入 §e/寻宝 区域名 " + + "§7保存"); + } +} diff --git a/backup/TreasureManager_backup_20260606_235145.java b/backup/TreasureManager_backup_20260606_235145.java new file mode 100644 index 0000000..b01a146 --- /dev/null +++ b/backup/TreasureManager_backup_20260606_235145.java @@ -0,0 +1,2920 @@ +package Sdf1_game; + +import org.bukkit.*; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.ItemStack; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; + +public class TreasureManager { + + private static final String CONFIG_SUFFIX = + ".sdf1.txt"; + + // ===== 模糊物品匹配 ===== + private final java.util.Map + ITEM_CN = new java.util.HashMap<>(); + + private void initItemMap() { + ITEM_CN.put("旋风棒", "BREEZE_ROD"); + ITEM_CN.put("旋风棍", "BREEZE_ROD"); + ITEM_CN.put("旋风杖", "BREEZE_ROD"); + ITEM_CN.put("烈焰棒", "BLAZE_ROD"); + ITEM_CN.put("烈焰棍", "BLAZE_ROD"); + ITEM_CN.put("金苹果", "GOLDEN_APPLE"); + ITEM_CN.put("附魔金苹果", + "ENCHANTED_GOLDEN_APPLE"); + ITEM_CN.put("下界合金剑", + "NETHERITE_SWORD"); + ITEM_CN.put("下界合金镐", + "NETHERITE_PICKAXE"); + ITEM_CN.put("下界合金锭", + "NETHERITE_INGOT"); + ITEM_CN.put("钻石剑", "DIAMOND_SWORD"); + ITEM_CN.put("钻石镐", + "DIAMOND_PICKAXE"); + ITEM_CN.put("钻石斧", "DIAMOND_AXE"); + ITEM_CN.put("钻石甲", + "DIAMOND_CHESTPLATE"); + ITEM_CN.put("钻石靴子", + "DIAMOND_BOOTS"); + ITEM_CN.put("钻石护腿", + "DIAMOND_LEGGINGS"); + ITEM_CN.put("钻石头盔", + "DIAMOND_HELMET"); + ITEM_CN.put("末影珍珠", + "ENDER_PEARL"); + ITEM_CN.put("绿宝石", "EMERALD"); + ITEM_CN.put("金锭", "GOLD_INGOT"); + ITEM_CN.put("铁剑", "IRON_SWORD"); + ITEM_CN.put("铁镐", "IRON_PICKAXE"); + ITEM_CN.put("铁锭", "IRON_INGOT"); + ITEM_CN.put("钻石", "DIAMOND"); + ITEM_CN.put("木棍", "STICK"); + ITEM_CN.put("面包", "BREAD"); + ITEM_CN.put("弓", "BOW"); + ITEM_CN.put("箭", "ARROW"); + ITEM_CN.put("盾牌", "SHIELD"); + ITEM_CN.put("不死图腾", + "TOTEM_OF_UNDYING"); + ITEM_CN.put("煤炭", "COAL"); + ITEM_CN.put("红石", "REDSTONE"); + ITEM_CN.put("附魔书", + "ENCHANTED_BOOK"); + } + + // ===== 领取限制 ===== + private final java.util.Map + claimDb = new java.util.HashMap<>(); + private File claimFile; + + // ===== 容器拦截计数 ===== + private final java.util.Map + containerBlockCount = + new java.util.HashMap<>(); + + + private final Main plugin; + private final Map + configs = new LinkedHashMap<>(); + private final Map + activeChests = new LinkedHashMap<>(); + private static final String MARKER = "SDF1"; + private final Set pendingReclaims = + new HashSet<>(); + private final java.util.Map + ITEM_CN_TO_ID = new java.util.HashMap<>(); + + + public Set getPendingReclaims() { + return pendingReclaims; + } + + + // ★ 领取记录 + private final Set claimedSet = + new HashSet<>(); + + // ★ 边框 + private BukkitTask borderTask; + private boolean borderShowing = false; + private String borderRegionName; + // ★ 容器拦截计数: 玩家名 → 次数 + + /** 容器拦截:第1-2次警告,第3次没收 */ + public boolean onContainerBlock( + String playerName) { + int count = containerBlockCount + .getOrDefault(playerName, 0) + 1; + containerBlockCount.put(playerName, count); + + plugin.getLogger().info( + "[寻宝] ★★★onContainerBlock: " + + playerName + + " 次数=" + count); + + if (count < 3) { + return false; + } else { + // ★ 重置计数 + containerBlockCount.put(playerName, 0); + return true; + } + } + + /** 重置容器拦截计数 */ + public void resetContainerBlock( + String playerName) { + containerBlockCount.remove(playerName); + } + + + public TreasureManager(Main plugin) { + this.plugin = plugin; + // ★ 必须在构造器里初始化 + this.claimFile = new File( + plugin.getDataFolder(), + "寻宝/领取限制.txt"); + claimFile.getParentFile().mkdirs(); + loadClaims(); + // 构造器里加(最后一行): + // ★ 每2秒扫描(全场景覆盖) + new org.bukkit.scheduler.BukkitRunnable() { + @Override + public void run() { + for (org.bukkit.entity.Player p : + plugin.getServer() + .getOnlinePlayers()) { + reclaimPlayerItems(p.getName()); + } + } + }.runTaskTimer(plugin, 40L, 40L); // 2秒 + initItemMap(); + loadClaims(); + + + } + + /** 模糊匹配物品名→材质ID */ + private String fuzzyMatchItem(String input) { + if (input == null || input.isEmpty()) + return null; + + // 精确匹配 + for (java.util.Map.Entry e + : ITEM_CN.entrySet()) { + if (input.equals(e.getKey())) + return e.getValue(); + } + + // 包含匹配 + String found = null; + for (java.util.Map.Entry e + : ITEM_CN.entrySet()) { + if (input.contains(e.getKey()) + || e.getKey().contains(input)) { + if (found != null) return null; + found = e.getValue(); + } + } + if (found != null) return found; + + // 前缀匹配 + if (input.length() >= 2) { + String p = input.substring(0, 2); + found = null; + for (java.util.Map.Entry e + : ITEM_CN.entrySet()) { + String k = e.getKey(); + if (k.length() >= 2 + && k.substring(0, 2) + .equals(p)) { + if (found != null) return null; + found = e.getValue(); + } + } + if (found != null) return found; + } + + return null; + } + + + /** 加载领取记录 */ + private void loadClaims() { + claimedSet.clear(); + if (!claimFile.exists()) return; + try { + BufferedReader br = new BufferedReader( + new InputStreamReader( + new FileInputStream(claimFile), + StandardCharsets.UTF_8)); + String line; + while ((line = br.readLine()) != null) { + line = line.trim(); + if (!line.isEmpty() + && !line.startsWith("#")) { + claimedSet.add(line); + } + } + br.close(); + } catch (IOException e) { + plugin.getLogger().warning( + "[寻宝] 加载领取记录失败"); + } + } + public void reload() { + forceCleanClaims(); + for (org.bukkit.entity.Player p : + plugin.getServer() + .getOnlinePlayers()) { + reclaimAllTreasureItems(p); + } + removeAllChests(); + configs.clear(); + activeChests.clear(); + refreshedChests.clear(); + loadAll(); + plugin.getLogger().info( + "[寻宝] ★重载完成," + + configs.size() + " 个区域"); + } + + // ★ 玩家寻宝临时数据 + private final Map + playerTreasureData = new HashMap<>(); + + public void setPlayerTreasureData( + String name, TreasureData data) { + playerTreasureData.put(name, data); + } + + public TreasureData getPlayerTreasureData( + String name) { + return playerTreasureData.get(name); + } + + public void clearPlayerTreasureData( + String name) { + playerTreasureData.remove(name); + } + + // 已刷新的宝箱坐标记录 + private final Set refreshedChests = + new HashSet<>(); + + /** 标记宝箱已刷新 */ + public void markRefreshed(Location loc) { + refreshedChests.add(locToString(loc)); + } + + /** 检查宝箱是否已刷新 */ + public boolean isRefreshed(Location loc) { + return refreshedChests.contains( + locToString(loc)); + } + + /** 清除刷新记录(重载/关闭时) */ + public void clearRefreshed() { + refreshedChests.clear(); + } + + private String locToString(Location loc) { + return loc.getWorld().getName() + + "," + loc.getBlockX() + + "," + loc.getBlockY() + + "," + loc.getBlockZ(); + } + + /** 回收玩家身上所有寻宝物品(重载/关闭时用) */ + public void reclaimAllTreasureItems( + org.bukkit.entity.Player player) { + if (player == null) return; + int removed = 0; + for (int i = 0; + i < player.getInventory().getSize(); i++) { + ItemStack item = + player.getInventory().getItem(i); + if (item == null) continue; + if (!TreasureInventory + .isCustomItem(item)) continue; + player.getInventory().setItem(i, null); + removed++; + } + if (removed > 0) { + player.sendMessage( + "§c[寻宝] 插件重载," + + removed + + "件寻宝物品已回收"); + } + } + + public void migrateOldFiles() { + File dir = new File( + plugin.getDataFolder(), "寻宝"); + dir.mkdirs(); + + File[] files = dir.listFiles( + (d, n) -> n.endsWith(".txt") + && !n.endsWith(CONFIG_SUFFIX)); + + if (files == null) return; + + // ★ 黑名单 + java.util.Set blacklist = + new java.util.HashSet<>(); + blacklist.add("领取限制"); + blacklist.add("配置说明"); + blacklist.add("设置"); + + for (File f : files) { + String baseName = f.getName() + .replace(".txt", ""); + + // ★ 黑名单跳过 + if (blacklist.contains(baseName)) { + continue; + } + + try { + BufferedReader br = + new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets.UTF_8)); + String first = br.readLine(); + br.close(); + + if (first != null + && first.trim() + .startsWith("区域名")) { + String newName = baseName + + CONFIG_SUFFIX; + File newFile = new File( + f.getParent(), newName); + if (f.renameTo(newFile)) { + plugin.getLogger().info( + "[寻宝] 迁移: " + + f.getName() + + " → " + + newName); + } + } + } catch (Exception ignored) { + } + } + } + + + // ========== 加载 ========== + + public void loadAll() { + configs.clear(); + activeChests.clear(); + File dir = new File( + plugin.getDataFolder(), "寻宝"); + dir.mkdirs(); + + // ★ 第一步:自动迁移旧.txt文件 + migrateOldFiles(dir); + + // 第二步:读取.sdf1.txt文件 + File[] files = dir.listFiles( + (d, n) -> n.endsWith(CONFIG_SUFFIX)); + + if (files == null) return; + + for (File f : files) { + try { + TreasureConfig tc = parseConfig(f); + if (tc != null && tc != null) { + configs.put(tc.name, tc); + plugin.getLogger().info( + "[寻宝] ★加载: " + + tc.name + + " 奖励数量=" + + tc.rewards.size()); + for (int i = 0; + i < tc.rewards.size(); i++) { + TreasureReward r = + tc.rewards.get(i); + plugin.getLogger().info( + "[寻宝] 奖励[" + i + + "]: " + + r.getType() + + " " + + r.getMaterialName() + + " x" + + r.getAmount() + + " 权重=" + + r.getWeight() + + " 名=" + + r.getDisplayName()); + } + loadSettings(tc); + } + } catch (Exception e) { + plugin.getLogger().warning( + "[寻宝] 加载失败: " + + f.getName() + + " " + + e.getMessage()); + } + } + + plugin.getLogger().info( + "[寻宝] 共" + + configs.size() + "个区域"); + } + + /** ★ 自动迁移旧.txt文件 → .sdf1.txt */ + private void migrateOldFiles(File dir) { + File[] oldFiles = dir.listFiles( + (d, name) -> name.endsWith(".txt") + && !name.endsWith(CONFIG_SUFFIX)); + + if (oldFiles == null) return; + + // ★ 黑名单:这些文件不处理 + java.util.Set blacklist = + new java.util.HashSet<>(); + blacklist.add("领取限制"); + blacklist.add("配置说明"); + blacklist.add("设置"); + + for (File f : oldFiles) { + String baseName = f.getName() + .replace(".txt", ""); + + // 黑名单跳过 + if (blacklist.contains(baseName)) continue; + + // 尝试读取第一行,确认是区域文件 + try { + BufferedReader br = + new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets.UTF_8)); + String first = br.readLine(); + br.close(); + + // 第一行包含"区域名"才迁移 + if (first != null + && first.contains("区域名")) { + String newName = + baseName + CONFIG_SUFFIX; + File newFile = new File( + f.getParent(), newName); + if (f.renameTo(newFile)) { + plugin.getLogger().info( + "[寻宝] ★迁移: " + + f.getName() + + " → " + + newName); + } + } + } catch (Exception ignored) { + } + } + } + + /** ★ 全场景扫描:过期/次数用完/非本人 → 全部回收 */ + /** ★ 全场景扫描回收 */ + public void reclaimPlayerItems( + String playerName) { + org.bukkit.entity.Player player = + plugin.getServer() + .getPlayerExact(playerName); + if (player == null) return; + + long now = System.currentTimeMillis(); + int removed = 0; + + for (int i = 0; + i < player.getInventory().getSize(); + i++) { + ItemStack item = + player.getInventory() + .getItem(i); + if (item == null) continue; + if (!TreasureInventory + .isCustomItem(item)) continue; + + String[] mark = + TreasureInventory + .parseMark(item); + if (mark == null) { + // ★ 无标记的自定义物品→直接清除 + player.getInventory() + .setItem(i, null); + removed++; + continue; + } + + String owner = mark[0]; + String region = mark[1]; + String rName = mark[2]; + int maxUse = Integer.parseInt(mark[3]); + int useCount = + Integer.parseInt(mark[4]); + long expireTime = + Long.parseLong(mark[5]); + + boolean shouldRemove = false; + String reason = ""; + + // 检查1: 非本人 + if (!owner.equalsIgnoreCase( + playerName)) { + shouldRemove = true; + reason = "非本人持有"; + } + + // ★ 检查2: 无领取记录→视为过期 + if (!shouldRemove + && !isClaimed(playerName, + rName, region)) { + shouldRemove = true; + reason = "无领取记录(过期/重载)"; + } + + // 检查3: 已过期 + if (!shouldRemove + && expireTime > 0 + && now >= expireTime) { + shouldRemove = true; + reason = "已过期"; + } + + // 检查4: 攻击次数用完 + if (!shouldRemove + && maxUse > 0 + && useCount >= maxUse) { + shouldRemove = true; + reason = "次数用完"; + } + + if (shouldRemove) { + player.getInventory() + .setItem(i, null); + unclaim(playerName, rName, region); + removed++; + if (!owner.equalsIgnoreCase( + playerName)) { + player.sendMessage( + "§c[寻宝] " + rName + + " 不属于你,已回收"); + } else { + player.sendMessage( + "§c[寻宝] " + rName + + " " + reason + + ",已回收"); + } + } + } + + if (removed > 0) { + plugin.getLogger().info( + "[寻宝] 回收: " + + playerName + + " " + removed + "件"); + } +} + + /** 领取限时物品 */ + public void claim(String player, + String itemName, + String regionName) { + String key = player + "." + itemName + + "." + regionName; + claimDb.put(key, true); + saveClaims(); + } + + /** 取消领取限制 */ + public void unclaim(String player, + String itemName, + String regionName) { + String key = player + "." + itemName + + "." + regionName; + claimDb.remove(key); + saveClaims(); + } + + /** 检查是否已领取 */ + public boolean isClaimed(String player, + String itemName, + String regionName) { + String key = player + "." + itemName + + "." + regionName; + return claimDb.containsKey(key); + } + + /** ★ 统一标点:中文→英文 */ + private String normalizePunctuation(String s) { + return s + .replace("\uff1a", ":") // :全角冒号 + .replace("\ufe58", ":") // ﹕小冒号 + .replace("\ua789", ":") // ꞉ + .replace(":", ":") // 冗余保险 + .replace("\uff0c", ",") // ,全角逗号 + .replace(",", ",") // 冗余保险 + .replace("\uff1b", ";") // ;全角分号 + .replace(";", ";") + .replace("\uff08", "(") // ( + .replace("(", "(") + .replace("\uff09", ")") // ) + .replace(")", ")") + .replace("\uff5e", "~") // ~全角波浪 + .replace("~", "~") + .replace("\u3001", ",") // 、顿号 + .replace("、", ",") + .replace("\uff01", "!") // ! + .replace("!", "!") + .replace("\uff1f", "?") // ? + .replace("?", "?") + .replace("\u3000", " ") // 全角空格 + .replace("\ufeff", ""); // BOM + } + + + private TreasureConfig parseConfig(File f) + throws IOException { + + TreasureConfig tc = new TreasureConfig( + f.getName().replace(".txt", "") + .replace(CONFIG_SUFFIX, "")); + + java.util.List lines = + new java.util.ArrayList<>(); + + BufferedReader br = new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets.UTF_8)); + String raw; + while ((raw = br.readLine()) != null) { + lines.add(raw); + } + br.close(); + + plugin.getLogger().info( + "[寻宝] 文件共 " + lines.size() + + " 行: " + f.getName()); + + for (int lineIdx = 0; + lineIdx < lines.size(); lineIdx++) { + + String rawLine = lines.get(lineIdx); + int lineNum = lineIdx + 1; + + String line = rawLine.trim(); + if (line.isEmpty() + || line.startsWith("#")) { + continue; + } + + // ★ 只转全角冒号为半角 + line = line.replace('\uff1a', ':'); + + int ci = line.indexOf(':'); + if (ci < 0) { + plugin.getLogger().warning( + "[寻宝] ★无冒号: " + line); + continue; + } + + String k = line.substring(0, ci).trim(); + String v = line.substring(ci + 1).trim(); + + plugin.getLogger().info( + "[寻宝] key=[" + k + + "] val=[" + v + "]"); + + switch (k) { + case "区域名": + tc.name = v; + break; + case "世界": + tc.world = v; + break; + case "坐标": + String[] pp = v.split(","); + if (pp.length == 4) { + int a = Integer.parseInt( + pp[0].trim()); + int b = Integer.parseInt( + pp[1].trim()); + int c = Integer.parseInt( + pp[2].trim()); + int d = Integer.parseInt( + pp[3].trim()); + tc.minX = Math.min(a, c); + tc.maxX = Math.max(a, c); + tc.minZ = Math.min(b, d); + tc.maxZ = Math.max(b, d); + } + break; + case "高度": + String[] yp = v.split(","); + if (yp.length == 2) { + tc.minY = Math.min( + Integer.parseInt( + yp[0].trim()), + Integer.parseInt( + yp[1].trim())); + tc.maxY = Math.max( + Integer.parseInt( + yp[0].trim()), + Integer.parseInt( + yp[1].trim())); + } + break; + case "最多宝箱": + tc.maxChests = + Integer.parseInt(v.trim()); + break; + case "检查间隔": + tc.spawnInterval = + parseTime(v); + break; + case "生成概率": + tc.spawnChance = + Integer.parseInt(v.trim()); + break; + case "随机高度": + tc.randomY = v.equals("是") + || v.equals("true"); + break; + case "奖励": + try { + TreasureReward rr = + parseReward(v); + if (rr != null) { + tc.rewards.add(rr); + } + } catch (Exception e) { + plugin.getLogger().warning( + "[寻宝] 奖励异常: " + + e.getMessage()); + } + break; + default: + plugin.getLogger().info( + "[寻宝] 未知key: [" + + k + "]"); + break; + } + } + + plugin.getLogger().info( + "[寻宝] 解析完成: " + + tc.name + + " 奖励=" + + tc.rewards.size()); + return tc; + } + + + private int parseTime(String s) { + s = s.trim().toLowerCase(); + if (s.matches("\\d+")) + return Integer.parseInt(s); + if (s.endsWith("分钟") || s.endsWith("m")) { + return Integer.parseInt( + s.replaceAll("[^\\d]", "")) * 60; + } + if (s.endsWith("小时") || s.endsWith("h")) { + return Integer.parseInt( + s.replaceAll("[^\\d]", "")) * 3600; + } + if (s.endsWith("秒") || s.endsWith("s")) { + return Integer.parseInt( + s.replaceAll("[^\\d]", "")); + } + try { return Integer.parseInt(s); } + catch (Exception e) { return 300; } + } + + + /** 显示已保存区域的粒子边框 */ + public boolean showRegionBorder(String name) { + TreasureConfig tc = configs.get(name); + if (tc == null) return false; + + World w = plugin.getServer() + .getWorld(tc.world); + if (w == null) return false; + + hideRegionBorder(); + normalize(tc); + + borderShowing = true; + borderRegionName = name; + + final int minX = tc.minX; + final int maxX = tc.maxX; + final int minZ = tc.minZ; + final int maxZ = tc.maxZ; + + plugin.getLogger().info( + "[寻宝] 显示边框: " + name + + " X=" + minX + "~" + maxX + + " Z=" + minZ + "~" + maxZ); + + borderTask = new BukkitRunnable() { + @Override + public void run() { + if (!borderShowing) return; + + List points = + new ArrayList<>(); + + for (int x = minX; x <= maxX; x++) { + points.add(getP(w, x, minZ)); + points.add(getP(w, x, maxZ)); + } + for (int z = minZ + 1; z < maxZ; z++) { + points.add(getP(w, minX, z)); + points.add(getP(w, maxX, z)); + } + + for (Player p : w.getPlayers()) { + int px = p.getLocation() + .getBlockX(); + int pz = p.getLocation() + .getBlockZ(); + if (px >= minX - 50 + && px <= maxX + 50 + && pz >= minZ - 50 + && pz <= maxZ + 50) { + for (Location loc : points) { + p.spawnParticle( + Particle.FLAME, + loc, 1, 0, 0, 0, 0); + } + } + } + } + }.runTaskTimer(plugin, 0L, 20L); + + return true; + } + + public void removeAllChests() { + int count = 0; + java.util.Iterator it = + activeChests.keySet().iterator(); + while (it.hasNext()) { + Location loc = it.next(); + it.remove(); + if (loc.getWorld() == null) continue; + if (loc.getBlock().getType() + == Material.CHEST) { + loc.getBlock() + .setType(Material.AIR); + count++; + } + } + if (count > 0) { + plugin.getLogger().info( + "[寻宝] 收回 " + count + " 个"); + } + // ★ 不清除领取记录 + } + + public void forceCleanClaims() { + // ★ 暴力方案:直接删文件再重建 + if (claimFile.exists()) { + boolean deleted = claimFile.delete(); + plugin.getLogger().info( + "[寻宝] ★删除领取记录: " + + deleted); + } + try { + claimFile.getParentFile().mkdirs(); + claimFile.createNewFile(); + plugin.getLogger().info( + "[寻宝] ★重建领取记录完成"); + } catch (IOException e) { + plugin.getLogger().warning( + "[寻宝] ★重建失败: " + + e.getMessage()); + } + claimedSet.clear(); + } + + + // ===== NBT 标记宝箱 ===== + + /** 生成宝箱时写入NBT标记 */ + private void tagChest(Location loc, + String regionName) { + org.bukkit.block.Block block = + loc.getBlock(); + if (!(block.getState() + instanceof org.bukkit.block.TileState)) + return; + org.bukkit.block.TileState state = + (org.bukkit.block.TileState) + block.getState(); + org.bukkit.persistence + .PersistentDataContainer pdc = + state.getPersistentDataContainer(); + org.bukkit.NamespacedKey kMarker = + new org.bukkit.NamespacedKey( + plugin, "sdf1_marker"); + org.bukkit.NamespacedKey kTime = + new org.bukkit.NamespacedKey( + plugin, "sdf1_time"); + org.bukkit.NamespacedKey kRegion = + new org.bukkit.NamespacedKey( + plugin, "sdf1_region"); + pdc.set(kMarker, + org.bukkit.persistence + .PersistentDataType.BOOLEAN, + true); + pdc.set(kTime, + org.bukkit.persistence + .PersistentDataType.LONG, + System.currentTimeMillis()); + pdc.set(kRegion, + org.bukkit.persistence + .PersistentDataType.STRING, + regionName); + state.update(true); + } + + /** 检查箱子是否是我们的 */ + private boolean isOurChest(Location loc) { + org.bukkit.block.Block block = + loc.getBlock(); + if (!(block.getState() + instanceof org.bukkit.block.TileState)) + return false; + org.bukkit.block.TileState state = + (org.bukkit.block.TileState) + block.getState(); + org.bukkit.persistence + .PersistentDataContainer pdc = + state.getPersistentDataContainer(); + org.bukkit.NamespacedKey kMarker = + new org.bukkit.NamespacedKey( + plugin, "sdf1_marker"); + return pdc.has(kMarker, + org.bukkit.persistence + .PersistentDataType.BOOLEAN); + } + + /** 读取箱子的区域名 */ + private String getChestRegion(Location loc) { + org.bukkit.block.Block block = + loc.getBlock(); + if (!(block.getState() + instanceof org.bukkit.block.TileState)) + return null; + org.bukkit.block.TileState state = + (org.bukkit.block.TileState) + block.getState(); + org.bukkit.persistence + .PersistentDataContainer pdc = + state.getPersistentDataContainer(); + org.bukkit.NamespacedKey kRegion = + new org.bukkit.NamespacedKey( + plugin, "sdf1_region"); + return pdc.getOrDefault(kRegion, + org.bukkit.persistence + .PersistentDataType.STRING, + ""); + } + + /** 读取箱子生成时间 */ + private long getChestTime(Location loc) { + org.bukkit.block.Block block = + loc.getBlock(); + if (!(block.getState() + instanceof org.bukkit.block.TileState)) + return 0; + org.bukkit.block.TileState state = + (org.bukkit.block.TileState) + block.getState(); + org.bukkit.persistence + .PersistentDataContainer pdc = + state.getPersistentDataContainer(); + org.bukkit.NamespacedKey kTime = + new org.bukkit.NamespacedKey( + plugin, "sdf1_time"); + return pdc.getOrDefault(kTime, + org.bukkit.persistence + .PersistentDataType.LONG, + 0L); + } + + // ★ 1. parseChineseReward + private TreasureReward parseChineseReward( + String line) { + try { + return parseChineseRewardInternal(line); + } catch (Exception e) { + plugin.getLogger().warning( + "[寻宝] ★中文解析异常: " + line + + " 错误: " + e.getMessage()); + return null; + } + } + private TreasureReward parseChineseRewardInternal( + String line) { + plugin.getLogger().info( + "[寻宝] 中文解析: " + line); + + // ★ 优先给玩家格式 + if (line.contains("给")) { + return parseGivePlayerItem(line); + } + // ★ 模糊匹配 + String matName = fuzzyMatchItem(line); + if (matName == null) { + plugin.getLogger().warning( + "[寻宝] ★无物品: " + line); + return null; + } + + String cnName = ""; + for (java.util.Map.Entry e + : ITEM_CN.entrySet()) { + if (e.getValue().equals(matName)) { + cnName = e.getKey(); + break; + } + } + + + // 数量 + int amount = 1; + java.util.regex.Matcher am = + java.util.regex.Pattern + .compile( + "([一二三四五六七八九十\\d]+)" + + "([个根把件套只])") + .matcher(line); + if (am.find()) { + amount = parseChineseNumber( + am.group(1)); + } + if (amount <= 1) { + java.util.regex.Matcher am2 = + java.util.regex.Pattern + .compile( + "(\\d+)\\s*[个根把件套只]") + .matcher(line); + if (am2.find()) { + amount = Integer.parseInt( + am2.group(1)); + } + } + + // 附魔 + String[] enchDefs = { + "水下速掘", "aqua_affinity", + "节肢杀手", "bane_of_arthropods", + "爆炸保护", "blast_protection", + "破甲", "breach", + "引雷", "channeling", + "绑定诅咒", "binding_curse", + "消失诅咒", "vanishing_curse", + "致密", "density", + "深海探索者", "depth_strider", + "效率", "efficiency", + "掉落缓冲", "feather_falling", + "火焰附加", "fire_aspect", + "火焰保护", "fire_protection", + "火矢", "flame", + "时运", "fortune", + "冰霜行者", "frost_walker", + "穿刺", "impaling", + "无限", "infinity", + "击退", "knockback", + "抢夺", "looting", + "忠诚", "loyalty", + "经验修补", "mending", + "多重射击", "multishot", + "穿透", "piercing", + "力量", "power", + "保护", "protection", + "冲击", "punch", + "快速装填", "quick_charge", + "水下呼吸", "respiration", + "激流", "riptide", + "锋利", "sharpness", + "精准采集", "silk_touch", + "亡灵杀手", "smite", + "灵魂疾行", "soul_speed", + "横扫之刃", "sweeping_edge", + "迅捷潜行", "swift_sneak", + "荆棘", "thorns", + "耐久", "unbreaking", + "风暴", "wind_burst", + }; + + List enchIds = new ArrayList<>(); + List enchLvls = new ArrayList<>(); + + for (int i = 0; + i < enchDefs.length; i += 2) { + String cn = enchDefs[i]; + String id = enchDefs[i + 1]; + if (!line.contains(cn)) continue; + + int lvl = 0; + java.util.regex.Matcher m1 = + java.util.regex.Pattern + .compile(cn + + "\\s*[,,]?\\s*等级?\\s*[::]?\\s*(\\d+)") + .matcher(line); + if (m1.find()) { + lvl = Integer.parseInt(m1.group(1)); + } + if (lvl <= 0) { + java.util.regex.Matcher m2 = + java.util.regex.Pattern + .compile(cn + "\\s*(\\d+)") + .matcher(line); + if (m2.find()) { + lvl = Integer.parseInt( + m2.group(1)); + } + } + if (lvl <= 0) { + java.util.regex.Matcher m3 = + java.util.regex.Pattern + .compile( + "(\\d+)\\s*级?" + cn) + .matcher(line); + if (m3.find()) { + lvl = Integer.parseInt( + m3.group(1)); + } + } + if (lvl <= 0) { + int pos = line.indexOf(cn); + if (pos >= 0) { + String after = line.substring( + pos + cn.length()); + java.util.regex.Matcher m4 = + java.util.regex.Pattern + .compile( + "\\D*(\\d+)") + .matcher(after); + if (m4.find()) { + int val = Integer.parseInt( + m4.group(1)); + if (val > 0 && val <= 255) { + lvl = val; + } + } + } + } + if (lvl <= 0) lvl = 1; + if (!enchIds.contains(id)) { + enchIds.add(id); + enchLvls.add(lvl); + } + } + + StringBuilder enchSB = new StringBuilder(); + for (int i = 0; i < enchIds.size(); i++) { + if (i > 0) enchSB.append(";"); + enchSB.append(enchIds.get(i)) + .append(",") + .append(enchLvls.get(i)); + } + String enchant = enchSB.length() > 0 + ? enchSB.toString() : null; + + // 时长 + int duration = 0; + java.util.regex.Matcher dm = + java.util.regex.Pattern + .compile( + "时长[::]?\\s*(\\d+)") + .matcher(line); + if (dm.find()) { + int val = Integer.parseInt(dm.group(1)); + duration = val < 100 ? val * 60 : val; + } + + // 攻击次数(可选) + int attackLimit = 0; + java.util.regex.Matcher atm = + java.util.regex.Pattern + .compile( + "攻击[^\\d]*[::]?\\s*(\\d+)") + .matcher(line); + if (atm.find()) { + attackLimit = Integer.parseInt( + atm.group(1)); + } + + // ★ 判断分支 + boolean hasEnch = enchant != null + && !enchant.isEmpty(); + + if (hasEnch || duration > 0 + || attackLimit > 0) { + return new TreasureReward( + TreasureReward.Type.ITEM, + matName, amount, 0, null, 10, + "§b" + cnName, + duration, enchant, null, + attackLimit); + } else { + return new TreasureReward( + TreasureReward.Type.ITEM, + matName, amount, 0, null, 10, + "§f" + cnName + " x" + amount); + } + } + + /** 处理 "给玩家XXX,附魔,等级,时长,攻击" 格式 */ + private TreasureReward parseGivePlayerItem( + String line) { + String itemName = line + .replace("给予玩家", "") + .replace("给玩家", "") + .replace("给予", "") + .replace("给", "") + .trim(); + + // ★ 模糊匹配 + String matName = fuzzyMatchItem(itemName); + if (matName == null) { + plugin.getLogger().warning( + "[寻宝] ★无法识别: " + line); + return null; + } + + String cnName = ""; + for (java.util.Map.Entry e + : ITEM_CN_TO_ID.entrySet()) { + if (e.getValue().equals(matName)) { + cnName = e.getKey(); + break; + } + } + + + String cleanName = itemName + .replaceAll( + "[一二三四五六七八九十\\d]+[个根把件套只]", + "") + .trim(); + + String[][] items = { + {"旋风棒", "BREEZE_ROD"}, + {"烈焰棒", "BLAZE_ROD"}, + {"附魔金苹果", "ENCHANTED_GOLDEN_APPLE"}, + {"金苹果", "GOLDEN_APPLE"}, + {"下界合金剑", "NETHERITE_SWORD"}, + {"下界合金镐", "NETHERITE_PICKAXE"}, + {"钻石剑", "DIAMOND_SWORD"}, + {"钻石镐", "DIAMOND_PICKAXE"}, + {"钻石甲", "DIAMOND_CHESTPLATE"}, + {"钻石靴子", "DIAMOND_BOOTS"}, + {"钻石护腿", "DIAMOND_LEGGINGS"}, + {"钻石头盔", "DIAMOND_HELMET"}, + {"末影珍珠", "ENDER_PEARL"}, + {"绿宝石", "EMERALD"}, + {"金锭", "GOLD_INGOT"}, + {"铁剑", "IRON_SWORD"}, + {"铁锭", "IRON_INGOT"}, + {"钻石", "DIAMOND"}, + {"木棍", "STICK"}, + {"面包", "BREAD"}, + {"不死图腾", "TOTEM_OF_UNDYING"}, + {"煤炭", "COAL"}, + {"红石", "REDSTONE"}, + }; + + String matName2 = null; + for (String[] pair : items) { + if (cleanName.contains(pair[0])) { + matName = pair[1]; + break; + } + } + if (matName == null) { + plugin.getLogger().warning( + "[寻宝] ★无法识别: " + line); + return null; + } + matName = matName.toUpperCase(); + + // 数量 + int amount = 1; + java.util.regex.Matcher am = + java.util.regex.Pattern + .compile( + "[一二三四五六七八九十\\d]+[个根把件套只]") + .matcher(itemName); + if (am.find()) { + String n = am.group() + .replaceAll( + "[个根把件套只]", ""); + amount = parseChineseNumber(n); + } + + // 附魔泛解析 + String[] enchDefs = { + "水下速掘", "aqua_affinity", + "节肢杀手", "bane_of_arthropods", + "爆炸保护", "blast_protection", + "破甲", "breach", + "引雷", "channeling", + "绑定诅咒", "binding_curse", + "消失诅咒", "vanishing_curse", + "致密", "density", + "深海探索者", "depth_strider", + "效率", "efficiency", + "掉落缓冲", "feather_falling", + "火焰附加", "fire_aspect", + "火焰保护", "fire_protection", + "火矢", "flame", + "时运", "fortune", + "冰霜行者", "frost_walker", + "穿刺", "impaling", + "无限", "infinity", + "击退", "knockback", + "抢夺", "looting", + "忠诚", "loyalty", + "海之眷顾", "luck_of_the_sea", + "突进", "lunge", + "诱饵", "lure", + "经验修补", "mending", + "多重射击", "multishot", + "穿透", "piercing", + "力量", "power", + "弹射物保护", + "projectile_protection", + "保护", "protection", + "冲击", "punch", + "快速装填", "quick_charge", + "水下呼吸", "respiration", + "激流", "riptide", + "锋利", "sharpness", + "精准采集", "silk_touch", + "亡灵杀手", "smite", + "灵魂疾行", "soul_speed", + "横扫之刃", "sweeping_edge", + "迅捷潜行", "swift_sneak", + "荆棘", "thorns", + "耐久", "unbreaking", + "风暴", "wind_burst", + "风爆", "wind_burst", + }; + + List enchIds = new ArrayList<>(); + List enchLvls = + new ArrayList<>(); + + for (int i = 0; + i < enchDefs.length; i += 2) { + String cn = enchDefs[i]; + String id = enchDefs[i + 1]; + if (!line.contains(cn)) continue; + + int lvl = 0; + + // "附魔火焰附加,等级255" + java.util.regex.Matcher m1 = + java.util.regex.Pattern + .compile(cn + + "[,,]?\\s*等级?\\s*[::]?\\s*(\\d+)") + .matcher(line); + if (m1.find()) { + lvl = Integer.parseInt( + m1.group(1)); + } + + // "击退255" + if (lvl <= 0) { + java.util.regex.Matcher m2 = + java.util.regex.Pattern + .compile(cn + + "\\s*(\\d+)") + .matcher(line); + if (m2.find()) { + lvl = Integer.parseInt( + m2.group(1)); + } + } + + // "255级锋利" + if (lvl <= 0) { + java.util.regex.Matcher m3 = + java.util.regex.Pattern + .compile( + "(\\d+)\\s*级?" + cn) + .matcher(line); + if (m3.find()) { + lvl = Integer.parseInt( + m3.group(1)); + } + } + + // 附魔名后最近的数字 + if (lvl <= 0) { + int pos = line.indexOf(cn); + if (pos >= 0) { + String after = line + .substring(pos + + cn.length()); + java.util.regex.Matcher m4 = + java.util.regex.Pattern + .compile( + "\\D*(\\d+)") + .matcher(after); + if (m4.find()) { + int val = + Integer.parseInt( + m4.group(1)); + if (val > 0 && val <= 255) { + lvl = val; + } + } + } + } + + if (lvl <= 0) lvl = 1; + + // 去重 + if (!enchIds.contains(id)) { + enchIds.add(id); + enchLvls.add(lvl); + } + } + + StringBuilder enchSB = + new StringBuilder(); + for (int i = 0; + i < enchIds.size(); i++) { + if (i > 0) enchSB.append(";"); + enchSB.append(enchIds.get(i)) + .append(",") + .append(enchLvls.get(i)); + } + String enchant = + enchSB.length() > 0 + ? enchSB.toString() : null; + + // 时长 + int duration = 0; + java.util.regex.Matcher dm = + java.util.regex.Pattern + .compile( + "时长[::]?\\s*([^,,。]+)") + .matcher(line); + if (dm.find()) { + duration = TreasureReward + .parseDuration( + dm.group(1).trim()); + } + + // 攻击次数 + int attackLimit = 0; + java.util.regex.Matcher atm = + java.util.regex.Pattern + .compile( + "攻击[^::]*[::]?\\s*(\\d+)") + .matcher(line); + if (atm.find()) { + attackLimit = Integer.parseInt( + atm.group(1)); + } + + plugin.getLogger().info( + "[寻宝] ★结果: " + + matName + " x" + amount + + " 附魔=" + enchant + + " 时长=" + duration + + " 攻击=" + attackLimit); + + return new TreasureReward( + TreasureReward.Type.ITEM, + matName, amount, 0, null, 10, + "§b" + cleanName, + duration, enchant, null, + attackLimit); + } + /** 获取容器拦截次数 */ + public int getContainerBlockCount( + String playerName) { + return containerBlockCount + .getOrDefault(playerName, 0); + } + + // ★ 2. isClaimed + private void saveClaims() { + if (claimFile == null) claimFile = new File( + plugin.getDataFolder(), "claims.yml"); + try { + FileWriter fw = new FileWriter( + claimFile); + for (String key : claimDb.keySet()) { + fw.write(key + "\n"); + } + fw.close(); + } catch (IOException e) { + plugin.getLogger().warning( + "[寻宝] 保存claims失败: " + + e.getMessage()); + } + } + + + // ★ 3. addClaim + public void addClaim(String player, + String itemName, + String regionName) { + String key = player + "|" + itemName + + "|" + regionName; + if (claimedSet.contains(key)) return; + claimedSet.add(key); + try { + OutputStreamWriter w = + new OutputStreamWriter( + new FileOutputStream( + claimFile, true), + StandardCharsets.UTF_8); + w.write(key + "\n"); + w.close(); + } catch (IOException e) { + plugin.getLogger().warning( + "[寻宝] 写入记录失败"); + } + } + + // ★ 4. removeClaim + public void removeClaim(String player, + String itemName, + String regionName) { + String key = player + "|" + itemName + + "|" + regionName; + claimedSet.remove(key); + rewriteClaimsFile(); + } + + // ★ 5. rewriteClaimsFile + private void rewriteClaimsFile() { + try { + OutputStreamWriter w = + new OutputStreamWriter( + new FileOutputStream( + claimFile), + StandardCharsets.UTF_8); + for (String e : claimedSet) + w.write(e + "\n"); + w.close(); + } catch (IOException ignored) { + } + } + + private int parseChineseNumber(String s) { + if (s == null || s.trim().isEmpty()) return 1; + s = s.trim().toLowerCase(); + + // ★ 直接数字 + try { return Integer.parseInt(s); } + catch (Exception ignore) {} + + // ★ 英文数字 + switch (s) { + case "one": return 1; + case "two": case "both": return 2; + case "three": return 3; + case "four": return 4; + case "five": return 5; + case "six": return 6; + case "seven": return 7; + case "eight": return 8; + case "nine": return 9; + case "ten": return 10; + case "eleven": return 11; + case "twelve": return 12; + case "thirteen": return 13; + case "fourteen": return 14; + case "fifteen": return 15; + case "sixteen": return 16; + case "seventeen": return 17; + case "eighteen": return 18; + case "nineteen": return 19; + case "twenty": return 20; + case "thirty": return 30; + case "forty": return 40; + case "fifty": return 50; + case "sixty": return 60; + case "seventy": return 70; + case "eighty": return 80; + case "ninety": return 90; + case "hundred": return 100; + } + + // ★ 罗马数字 + int roman = parseRoman(s); + if (roman > 0) return roman; + + // ★ 中文数字 + int cn = parseFullChinese(s); + if (cn > 0) return cn; + + // ★ 兜底:提取字符串中第一个数字 + java.util.regex.Matcher m = + java.util.regex.Pattern + .compile("(\\d+)") + .matcher(s); + if (m.find()) { + try { + return Integer.parseInt(m.group(1)); + } catch (Exception ignore) {} + } + + return 1; + } + + /** ★ 罗马数字解析 */ + private int parseRoman(String s) { + s = s.trim().toUpperCase(); + if (s.isEmpty()) return 0; + // 简单验证:只含IVXLCDM + if (!s.matches("[IVXLCDM]+")) return 0; + + int result = 0; + int prev = 0; + for (int i = s.length() - 1; i >= 0; i--) { + int val; + switch (s.charAt(i)) { + case 'I': val = 1; break; + case 'V': val = 5; break; + case 'X': val = 10; break; + case 'L': val = 50; break; + case 'C': val = 100; break; + case 'D': val = 500; break; + case 'M': val = 1000; break; + default: return 0; + } + if (val < prev) { + result -= val; + } else { + result += val; + } + prev = val; + } + return result > 0 && result <= 9999 + ? result : 0; + } + + /** ★ 完整中文数字解析 */ + private int parseFullChinese(String s) { + s = s.trim(); + + // ★ 简单中文数字(预定义) + String[][] cnNums = { + {"零", "0"}, {"〇", "0"}, + {"一", "1"}, {"壹", "1"}, + {"二", "2"}, {"两", "2"}, {"贰", "2"}, + {"三", "3"}, {"叁", "3"}, + {"四", "4"}, {"肆", "4"}, + {"五", "5"}, {"伍", "5"}, + {"六", "6"}, {"陆", "6"}, + {"七", "7"}, {"柒", "7"}, + {"八", "8"}, {"捌", "8"}, + {"九", "9"}, {"玖", "9"}, + {"十", "10"}, {"拾", "10"}, + {"十一", "11"}, {"十二", "12"}, + {"十三", "13"}, {"十四", "14"}, + {"十五", "15"}, {"十六", "16"}, + {"十七", "17"}, {"十八", "18"}, + {"十九", "19"}, + {"二十", "20"}, {"廿", "20"}, + {"二十一", "21"}, {"二十二", "22"}, + {"二十三", "23"}, {"二十四", "24"}, + {"二十五", "25"}, {"二十六", "26"}, + {"二十七", "27"}, {"二十八", "28"}, + {"二十九", "29"}, + {"三十", "30"}, {"卅", "30"}, + {"四十", "40"}, {"五十", "50"}, + {"六十", "60"}, {"七十", "70"}, + {"八十", "80"}, {"九十", "90"}, + {"一百", "100"}, {"一百", "100"}, + {"一千", "1000"}, {"一万", "10000"}, + {"一亿", "100000000"}, + }; + for (String[] pair : cnNums) { + if (s.equals(pair[0])) { + return Integer.parseInt(pair[1]); + } + } + + // ★ 复杂中文数字(二十、二百五、三千...) + int result = 0; + int current = 0; + + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + int digit = cnCharToDigit(c); + if (digit >= 0) { + current = current * 10 + digit; + } else if (c == '十' || c == '拾') { + if (current == 0) current = 1; + result += current * 10; + current = 0; + } else if (c == '百' || c == '佰') { + if (current == 0) current = 1; + result += current * 100; + current = 0; + } else if (c == '千' || c == '仟') { + if (current == 0) current = 1; + result += current * 1000; + current = 0; + } else if (c == '万' || c == '萬') { + if (current == 0) current = 1; + result = (result + current) * 10000; + current = 0; + } else if (c == '亿' || c == '億') { + if (current == 0) current = 1; + result = (result + current) + * 100000000; + current = 0; + } + } + result += current; + + return result > 0 ? result : 0; + } + + + /** 中文单字→数字 */ + private int cnCharToDigit(char c) { + switch (c) { + case '零': case '〇': return 0; + case '一': case '壹': return 1; + case '二': case '两': case '贰': + return 2; + case '三': case '叁': return 3; + case '四': case '肆': return 4; + case '五': case '伍': return 5; + case '六': case '陆': return 6; + case '七': case '柒': return 7; + case '八': case '捌': return 8; + case '九': case '玖': return 9; + case '廿': return 2; + case '卅': return 3; + default: return -1; + } + } + + private String matchChineseEnchant(String name) { + name = name.trim(); + switch (name) { + case "击退": return "KNOCKBACK"; + case "锋利": return "SHARPNESS"; + case "火焰附加": return "FIRE_ASPECT"; + case "抢夺": return "LOOTING"; + case "效率": return "EFFICIENCY"; + case "时运": return "FORTUNE"; + case "耐久": return "UNBREAKING"; + case "经验修补": return "MENDING"; + case "保护": return "PROTECTION"; + case "力量": return "POWER"; + case "火矢": return "FLAME"; + case "无限": return "INFINITY"; + case "忠诚": return "LOYALTY"; + default: return name.toUpperCase().replace(" ", "_"); + } + } + + // ★ 只在玩家攻击玩家时计数 + // ★ 只拦截自定义物品放入容器 + @org.bukkit.event.EventHandler(priority = + org.bukkit.event.EventPriority.HIGH) + public void onClick( + org.bukkit.event.inventory + .InventoryClickEvent e) { + if (e.getWhoClicked() + instanceof org.bukkit.entity.Player) { + org.bukkit.entity.Player p = + (org.bukkit.entity.Player) + e.getWhoClicked(); + + // ★ 检查光标上的物品 + // ★ 检查光标物品(容器放入) + if (e.getCursor() != null + && TreasureInventory + .isCustomItem(e.getCursor())) { + org.bukkit.entity.Player p0 = + (org.bukkit.entity.Player) + e.getWhoClicked(); + InventoryType topType = + e.getView().getTopInventory() + .getType(); + if (topType != InventoryType.PLAYER + && topType + != InventoryType.CRAFTING + && topType + != InventoryType.CREATIVE) { + e.setCancelled(true); + // ★ 事不过三 + boolean confiscate = + plugin.getTreasureManager() + .onContainerBlock( + p.getName()); + if (confiscate) { + // 第3次:没收光标物品 + e.getCursor().setAmount(0); + p.sendMessage( + "§c[寻宝] 多次违规," + + "物品已被没收!"); + plugin.getLogger().info( + "[寻宝] ★没收: " + + p.getName()); + } + return; + } + } + + // ★ 检查槽位物品(归属+容器) + ItemStack clicked = e.getCurrentItem(); + if (clicked != null + && TreasureInventory + .isCustomItem(clicked)) { + org.bukkit.entity.Player p2 = + (org.bukkit.entity.Player) + e.getWhoClicked(); + // 非本人→直接没收 + String[] mark = + TreasureInventory + .parseMark(clicked); + if (mark != null + && !mark[0].equalsIgnoreCase( + p.getName())) { + e.setCancelled(true); + e.getCurrentItem().setAmount(0); + p.sendMessage( + "§c[寻宝] 该物品不属于你,已没收"); + return; + } + // 目标是容器→事不过三 + InventoryType topType = + e.getView().getTopInventory() + .getType(); + if (topType != InventoryType.PLAYER + && topType + != InventoryType.CRAFTING + && topType + != InventoryType.CREATIVE) { + e.setCancelled(true); + boolean confiscate = + plugin.getTreasureManager() + .onContainerBlock( + p.getName()); + if (confiscate) { + e.getCurrentItem() + .setAmount(0); + p.sendMessage( + "§c[寻宝] 多次违规," + + "物品已被没收!"); + } + } + } + + + // ★ 检查被点击槽位的物品 + ItemStack clicked1 = e.getCurrentItem(); + if (clicked != null + && TreasureInventory + .isCustomItem(clicked)) { + String[] mark = + TreasureInventory.parseMark( + clicked); + if (mark != null + && !mark[0] + .equalsIgnoreCase( + p.getName())) { + // 槽位物品不属于本人→回收 + e.setCancelled(true); + e.getCurrentItem().setAmount(0); + p.sendMessage( + "§c[寻宝] 该物品不属于你"); + } + } + } + } + + // ★ 拖拽拦截 + @org.bukkit.event.EventHandler(priority = + org.bukkit.event.EventPriority.HIGH) + public void onDrag( + org.bukkit.event.inventory + .InventoryDragEvent e) { + if (!(e.getWhoClicked() + instanceof org.bukkit.entity.Player)) + return; + org.bukkit.entity.Player p = + (org.bukkit.entity.Player) + e.getWhoClicked(); + for (ItemStack item : + e.getNewItems().values()) { + if (TreasureInventory + .isCustomItem(item)) { + InventoryType type = e.getView() + .getTopInventory() + .getType(); + if (type != InventoryType.PLAYER + && type != InventoryType.CRAFTING) { + e.setCancelled(true); + p.sendMessage( + "§c[寻宝] 该物品不可放入容器"); + return; + } + } + } + } + + // ★ 潜影盒/容器右键拦截 + @org.bukkit.event.EventHandler(priority = + org.bukkit.event.EventPriority.HIGH) + public void onInteract( + org.bukkit.event.player + .PlayerInteractEvent e) { + if (e.getItem() == null) return; + if (!TreasureInventory + .isCustomItem(e.getItem())) return; + if (!e.getAction().name() + .contains("RIGHT_CLICK")) return; + if (e.getClickedBlock() == null) return; + String btype = e.getClickedBlock() + .getType().name(); + if (btype.contains("SHULKER") + || btype.contains("CHEST") + || btype.equals("BARREL") + || btype.equals("ENDER_CHEST") + || btype.equals("HOPPER") + || btype.equals("DROPPER") + || btype.equals("DISPENSER")) { + e.setCancelled(true); + e.getPlayer().sendMessage( + "§c[寻宝] 该物品不可放入容器"); + } + } + + // ★ 铁砧拦截 + @org.bukkit.event.EventHandler(priority = + org.bukkit.event.EventPriority.HIGH) + public void onAnvil( + org.bukkit.event.inventory + .PrepareAnvilEvent e) { + ItemStack first = + e.getInventory().getItem(0); + if (first != null + && TreasureInventory + .isCustomItem(first)) { + e.setResult(null); + } + } + + /** 隐藏边框 */ + public boolean hideRegionBorder() { + if (borderTask != null) { + borderTask.cancel(); + borderTask = null; + } + borderShowing = false; + borderRegionName = null; + return true; + } + + /** 按名字隐藏(兼容旧调用) */ + public boolean hideRegionBorder(String name) { + if (name == null + || name.equals(borderRegionName)) { + return hideRegionBorder(); + } + return false; + } + + /** 检查边框是否在显示 */ + public boolean isBorderVisible(String name) { + return borderShowing + && name.equals(borderRegionName); + } + + private Location getP(World w, int x, int z) { + int y = w.getHighestBlockYAt(x, z) + 2; + return new Location(w, x + 0.5, y, z + 0.5); + } + + /** + * 格式: + * DIAMOND,1,10 + * 债券,50,10 + * 命令,give {player} diamond 1,5 + * 贴标:STICK,1,60,KNOCKBACK,2,鸡腿棒,10 + * 物品,数量,权重,附魔,等级,显示名,时长秒 + */ + private TreasureReward parseReward(String line) { + // ★ 只转冒号,不动逗号 + line = line.replace('\uff1a', ':'); + + plugin.getLogger().info( + "[寻宝] 解析奖励: " + line); + + if (line.charAt(0) == '\uFEFF') + line = line.substring(1); + + // 贴标 + if (line.startsWith("贴标:")) { + return parseCustomItem( + line.substring("贴标:".length())); + } + + String upper = line.toUpperCase(); + // ★ 债券 + if (upper.contains("债券")) { + String after = line + .replaceAll("(?i)债券", "") + .trim(); + + int bMin = 1; + int bMax = 1; + int w = 25; + + if (!after.isEmpty()) { + String[] parts = after.split(","); + String rangePart = parts[0].trim(); + + if (parts.length >= 2) { + try { + w = parseChineseNumber( + parts[1].trim()); + } catch (Exception ignore) { + } + } + + // ★ 范围:~ 或 - 分隔 + if (rangePart.contains("~")) { + String[] r = + rangePart.split("~"); + bMin = parseChineseNumber( + r[0].trim()); + bMax = parseChineseNumber( + r[1].trim()); + } else if (rangePart.contains("-")) { + String[] r = + rangePart.split("-"); + bMin = parseChineseNumber( + r[0].trim()); + bMax = parseChineseNumber( + r[1].trim()); + } else { + bMin = parseChineseNumber( + rangePart); + bMax = bMin; + } + } + + final int fbMin = bMin; + final int fbMax = bMax; + final int fw = w; + + TreasureReward tr = + new TreasureReward( + TreasureReward.Type.BOND, + null, 1, fbMin, null, fw, + "§6" + fbMin + + "~" + fbMax + + "债券"); + tr.setBondRange(fbMin, fbMax); + + plugin.getLogger().info( + "[寻宝] → 债券: " + + fbMin + "~" + fbMax + + " 权重=" + fw); + return tr; + } + + + // 命令 + if (upper.contains("命令")) { + int ci = line.indexOf(','); + if (ci < 0) ci = line.indexOf(':'); + String cmd = ci > 0 + ? line.substring(ci + 1).trim() + : ""; + int w = 10; + if (cmd.contains(",")) { + String[] parts = cmd.split(","); + cmd = parts[0].trim(); + w = Integer.parseInt( + parts[1].trim()); + } + return new TreasureReward( + TreasureReward.Type.COMMAND, + null, 1, 0, cmd, w, + "§a命令奖励"); + } + + // 中文分支(给玩家/附魔/时长/攻击) + if (line.contains("给") + || line.contains("附魔") + || line.contains("时长") + || line.contains("攻击")) { + return parseGivePlayerItem(line); + } + + // 中文物品简写(面包,64,10) + String[] commaParts = line.split(","); + if (commaParts.length >= 2) { + String firstPart = + commaParts[0].trim(); + boolean hasCN = false; + for (char c : firstPart.toCharArray()) { + if (c > 0x4E00) { + hasCN = true; + break; + } + } + if (hasCN) { + return parseChineseReward(line); + } + } + + // 英文格式(DIAMOND,1,10) + line = line.replace(",", ","); + String[] p = line.split(","); + if (p.length < 2) return null; + String type = p[0].trim().toUpperCase(); + + int amt = Integer.parseInt(p[1].trim()); + int w = p.length > 2 + ? Integer.parseInt(p[2].trim()) + : 10; + + String[][] cnNames = { + {"DIAMOND", "钻石"}, + {"GOLD_INGOT", "金锭"}, + {"IRON_INGOT", "铁锭"}, + {"EMERALD", "绿宝石"}, + {"COAL", "煤炭"}, + {"REDSTONE", "红石"}, + {"DIAMOND_SWORD", "钻石剑"}, + {"DIAMOND_PICKAXE", "钻石镐"}, + {"DIAMOND_AXE", "钻石斧"}, + {"DIAMOND_CHESTPLATE", "钻石甲"}, + {"DIAMOND_BOOTS", "钻石靴子"}, + {"DIAMOND_LEGGINGS", "钻石护腿"}, + {"DIAMOND_HELMET", "钻石头盔"}, + {"NETHERITE_SWORD", "下界合金剑"}, + {"NETHERITE_PICKAXE", + "下界合金镐"}, + {"NETHERITE_INGOT", + "下界合金锭"}, + {"BLAZE_ROD", "烈焰棒"}, + {"BREEZE_ROD", "旋风棒"}, + {"ENDER_PEARL", "末影珍珠"}, + {"BREAD", "面包"}, + {"STICK", "木棍"}, + {"BOW", "弓"}, + {"ARROW", "箭"}, + {"SHIELD", "盾牌"}, + }; + + String cnName = type; + for (String[] pair : cnNames) { + if (pair[0].equals(type)) { + cnName = pair[1]; + break; + } + } + + return new TreasureReward( + TreasureReward.Type.ITEM, + type, amt, 0, null, w, + "§f" + cnName + " x" + amt); + } + + public void saveConfig(TreasureConfig tc) { + File dir = new File( + plugin.getDataFolder(), "寻宝"); + dir.mkdirs(); + File f = new File(dir, + tc.name + ".sdf1.txt"); + try { + OutputStreamWriter w = + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8); + w.write("区域名: " + tc.name + "\n"); + w.write("世界: " + tc.world + "\n"); + w.write("坐标: " + tc.minX + "," + + tc.minZ + "," + + tc.maxX + "," + + tc.maxZ + "\n"); + w.write("高度: " + tc.minY + "," + + tc.maxY + "\n"); + w.write("最多宝箱: " + tc.maxChests + "\n"); + w.write("检查间隔: " + + tc.spawnInterval + "秒\n"); + w.write("生成概率: " + tc.spawnChance + "\n"); + w.write("随机高度: " + + (tc.randomY ? "是" : "否") + "\n"); + for (TreasureReward r : tc.rewards) { + w.write("奖励: " + rewardToString(r) + "\n"); + } + w.close(); + } catch (IOException e) { + plugin.getLogger().warning( + "[寻宝] 保存失败: " + e.getMessage()); + } + } + + private String rewardToString(TreasureReward r) { + if (r.getType() == TreasureReward.Type.BOND) { + return "债券," + r.getBondMin() + + "~" + r.getBondMax() + + "," + r.getWeight(); + } + if (r.getType() == TreasureReward.Type.COMMAND) { + return "命令," + r.getCommand() + + "," + r.getWeight(); + } + String name = r.getMaterialName(); + int amt = r.getAmount(); + int w = r.getWeight(); + return name + "," + amt + "," + w; + } + + public void loadSettings(TreasureConfig tc) { + + File f = new File( + plugin.getDataFolder(), + "寻宝/设置.txt"); + if (!f.exists()) { + File dir = new File( + plugin.getDataFolder(), + "寻宝"); + FileGenerator + .ensureTreasureConfig(dir); + } + if (!f.exists()) return; + try (BufferedReader br = + new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets.UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + line = line.trim(); + if (line.isEmpty() + || line.startsWith("#")) + continue; + line = normalizePunctuation(line); + int ci = line.indexOf(':'); + if (ci < 0) continue; + String k = line.substring(0, ci) + .trim(); + String v = line.substring(ci + 1) + .trim(); + switch (k) { + case "保底债券": + tc.guaranteeBond = + v.equals("是") + || v.equals("true"); + break; + case "随机数量": + String[] rr = + v.split("[~\\-]"); + if (rr.length == 2) { + tc.randomMin = + Integer.parseInt( + rr[0].trim()); + tc.randomMax = + Integer.parseInt( + rr[1].trim()); + } + break; + case "刷新费用": + tc.refreshBondCost = + Integer.parseInt( + v.trim()); + break; + case "刷新最低奖励": + tc.refreshMinRewards = + Integer.parseInt( + v.trim()); + break; + } + } + + + } catch (Exception e) { + plugin.getLogger().warning( + "[寻宝] 设置加载失败: " + + e.getMessage()); + } + plugin.getLogger().info( + "[寻宝] 设置: 保底=" + + tc.guaranteeBond + + " 随机=" + tc.randomMin + + "~" + tc.randomMax); + } + + /** + * 贴标物品解析 + * 格式: 物品ID,数量,权重,附魔ID,附魔等级,显示名,时长秒 + * 例: STICK,1,60,KNOCKBACK,2,鸡腿棒,3600 + */ + private TreasureReward parseCustomItem(String line) { + String[] p = line.split(","); + if (p.length < 6) return null; + + String matName = p[0].trim(); + int amt = Integer.parseInt(p[1].trim()); + int weight = Integer.parseInt(p[2].trim()); + String enchId = p[3].trim(); + int enchLvl = Integer.parseInt(p[4].trim()); + String displayName = p[5].trim(); + int duration = p.length >= 7 + ? parseTime(p[6].trim()) : 3600; + + Material mat = + Material.matchMaterial(matName); + if (mat == null) mat = Material.STICK; + + String enchant = enchId + "," + enchLvl; + String lore = "§7" + displayName; + + return new TreasureReward( + TreasureReward.Type.ITEM, + matName, amt, 0, null, weight, + "§b" + displayName, + duration, enchant, lore); + } + + + // ========== 保存区域 ========== + + public boolean saveRegion(String name, + String world, + int minX, int minZ, + int maxX, int maxZ, + int minY, int maxY) { + + File dir = new File( + plugin.getDataFolder(), "寻宝"); + dir.mkdirs(); + + // ★ 确认后缀是 .sdf1.txt + File f = new File(dir, + name + ".sdf1.txt"); + + try { + OutputStreamWriter w = + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8); + w.write("区域名: " + name + "\n"); + w.write("世界: " + world + "\n"); + w.write("坐标: " + minX + "," + + minZ + "," + + maxX + "," + maxZ + "\n"); + w.write("高度: " + minY + "," + + maxY + "\n"); + w.write("最多宝箱: 2\n"); + w.write("检查间隔: 5分钟\n"); + w.write("生成概率: 100\n"); + w.write("奖励: DIAMOND,1,10\n"); + w.close(); + } catch (IOException e) { + plugin.getLogger().warning( + "[寻宝] 保存失败: " + + e.getMessage()); + return false; + } + + // ★ 加入内存map + TreasureConfig tc = new TreasureConfig(name); + tc.world = world; + tc.minX = minX; + tc.maxX = maxX; + tc.minZ = minZ; + tc.maxZ = maxZ; + tc.minY = minY; + tc.maxY = maxY; + configs.put(name, tc); + + return true; + } + + + /** 标准化坐标:确保 minX <= maxX, minZ <= maxZ */ + private void normalize(TreasureConfig tc) { + int a = Math.min(tc.minX, tc.maxX); + int b = Math.max(tc.minX, tc.maxX); + tc.minX = a; + tc.maxX = b; + a = Math.min(tc.minZ, tc.maxZ); + b = Math.max(tc.minZ, tc.maxZ); + tc.minZ = a; + tc.maxZ = b; + } + + + public boolean removeRegion(String name) { + File f = new File(plugin.getDataFolder(), + "寻宝/" + name + ".txt"); + boolean ok = f.exists() && f.delete(); + loadAll(); + return ok; + } + // ★ expand: 四周各扩N格 + public boolean expandRegion(String name, int n) { + TreasureConfig tc = configs.get(name); + if (tc == null) return false; + tc.minX -= n; + tc.minZ -= n; + tc.maxX += n; + tc.maxZ += n; + rewriteFile(tc); + return true; + } + + // ★ return: 四周各缩N格 + public boolean returnRegion(String name, int n) { + TreasureConfig tc = configs.get(name); + if (tc == null) return false; + if (tc.maxX - tc.minX <= n * 2 + || tc.maxZ - tc.minZ <= n * 2) { + return false; + } + tc.minX += n; + tc.minZ += n; + tc.maxX -= n; + tc.maxZ -= n; + rewriteFile(tc); + return true; + } + + private void rewriteFile(TreasureConfig tc) { + File f = new File(plugin.getDataFolder(), + "寻宝/" + tc.name + ".txt"); + List lines = new ArrayList<>(); + lines.add("区域名: " + tc.name); + lines.add("世界: " + tc.world); + lines.add("坐标: " + tc.minX + "," + + tc.minZ + "," + tc.maxX + + "," + tc.maxZ); + lines.add("高度: " + tc.minY + "," + + tc.maxY); + lines.add("最多宝箱: " + tc.maxChests); + lines.add("检查间隔: " + tc.spawnInterval); + lines.add("生成概率: " + tc.spawnChance); + lines.add("随机高度: " + + (tc.randomY ? "是" : "否")); + for (TreasureReward r : tc.rewards) { + switch (r.getType()) { + case BOND: + lines.add("奖励: 债券," + + r.getBondAmount() + + "," + r.getWeight()); + break; + case COMMAND: + lines.add("奖励: 命令," + + r.getCommand() + + "," + r.getWeight()); + break; + case ITEM: + lines.add("奖励: " + + r.getMaterialName() + + "," + r.getAmount() + + "," + r.getWeight()); + break; + } + } + try { + OutputStreamWriter w = + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8); + for (String l : lines) + w.write(l + "\n"); + w.close(); + } catch (IOException ignored) { + } + } + + // ========== 宝箱操作 ========== + + public int getChestCount(String name) { + int c = 0; + for (String v : activeChests.values()) { + if (v.equals(name)) c++; + } + return c; + } + + public boolean spawnChest(TreasureConfig tc) { + World w = plugin.getServer() + .getWorld(tc.world); + if (w == null) return false; + + normalize(tc); + + // 统计现有宝箱 + java.util.List existing = + new ArrayList<>(); + for (java.util.Map.Entry e + : activeChests.entrySet()) { + if (e.getValue().equals(tc.name) + && e.getKey().getWorld() != null + && e.getKey().getWorld().getName() + .equals(tc.world)) { + existing.add(e.getKey()); + } + } + + // 超过上限→销毁最早的 + while (existing.size() >= tc.maxChests) { + Location oldest = existing.remove(0); + if (oldest.getBlock().getType() + == Material.CHEST) { + oldest.getBlock() + .setType(Material.AIR); + } + activeChests.remove(oldest); + } + + // ★ 50次尝试 + for (int attempt = 0; + attempt < 50; attempt++) { + int x = ThreadLocalRandom.current() + .nextInt(tc.minX, tc.maxX + 1); + int z = ThreadLocalRandom.current() + .nextInt(tc.minZ, tc.maxZ + 1); + int y = ThreadLocalRandom.current() + .nextInt(tc.minY, tc.maxY + 1); + + // ★ 智能搜索2格空气 + Location spot = findTwoAirSpot( + w, x, y, z, tc); + if (spot == null) continue; + + Block block = spot.getBlock(); + block.setType(Material.CHEST); + tagChest(spot, tc.name); + activeChests.put(spot, tc.name); + + plugin.getLogger().info( + "[寻宝] ★生成: " + tc.name + + " [" + spot.getBlockX() + + "," + spot.getBlockY() + + "," + spot.getBlockZ() + + "]"); + + if (borderShowing + && borderRegionName != null + && borderRegionName + .equals(tc.name)) { + showRegionBorder(tc.name); + } + return true; + } + + plugin.getLogger().warning( + "[寻宝] ★生成失败: " + tc.name + + " 50次未找到位置"); + return false; + } + + /** ★ 从指定坐标开始,上下搜索2格空气 */ + private Location findTwoAirSpot( + World w, int x, int startY, int z, + TreasureConfig tc) { + // 先检查原位 + Location origin = + new Location(w, x, startY, z); + if (origin.getBlock().getType() + == Material.AIR + && origin.getBlock() + .getRelative( + org.bukkit.block.BlockFace.UP) + .getType() == Material.AIR) { + return origin; + } + + // ★ 从原位向下搜索 + for (int y = startY - 1; + y >= tc.minY; y--) { + Location loc = + new Location(w, x, y, z); + Block bottom = loc.getBlock(); + Block top = bottom.getRelative( + org.bukkit.block.BlockFace.UP); + if (bottom.getType() == Material.AIR + && top.getType() + == Material.AIR) { + return loc; + } + } + + // ★ 从原位向上搜索 + for (int y = startY + 1; + y <= tc.maxY; y++) { + Location loc = + new Location(w, x, y, z); + Block bottom = loc.getBlock(); + Block top = bottom.getRelative( + org.bukkit.block.BlockFace.UP); + if (bottom.getType() == Material.AIR + && top.getType() + == Material.AIR) { + return loc; + } + } + + return null; + } + /** ★ 公布宝箱坐标 */ + public boolean announceChest() { + return announceChest(null); + } + + /** ★ 公布指定区域最旧宝箱 */ + public boolean announceChest( + String regionName) { + if (activeChests.isEmpty()) return false; + + Location oldest = null; + String oldestRegion = null; + long oldestTime = Long.MAX_VALUE; + + for (java.util.Map.Entry e + : activeChests.entrySet()) { + Location loc = e.getKey(); + String region = e.getValue(); + if (loc.getWorld() == null) continue; + if (loc.getBlock().getType() + != Material.CHEST) continue; + + // ★ 指定了区域就只找该区域 + if (regionName != null + && !regionName + .equalsIgnoreCase(region)) { + continue; + } + + long time = getChestTime(loc); + if (time < oldestTime) { + oldestTime = time; + oldest = loc; + oldestRegion = region; + } + } + + if (oldest == null) return false; + + int cx = oldest.getBlockX(); + int cz = oldest.getBlockZ(); + + for (org.bukkit.entity.Player p + : plugin.getServer() + .getOnlinePlayers()) { + p.sendMessage("§6§l[寻宝] §e" + + (oldestRegion != null + ? oldestRegion : "") + + " §a宝藏出现在 X:" + + cx + " Z:" + cz); + p.sendMessage("§7快去寻找吧!"); + } + + plugin.getLogger().info( + "[寻宝] ★公布: " + oldestRegion + + " [" + cx + "," + cz + "]"); + return true; + } + + /** 清除带NBT标记但不在记录中的残留宝箱 */ + public void cleanResidualChests() { + int total = 0; + for (TreasureConfig tc + : configs.values()) { + World w = plugin.getServer() + .getWorld(tc.world); + if (w == null) continue; + normalize(tc); + for (int x = tc.minX; + x <= tc.maxX; x++) { + for (int z = tc.minZ; + z <= tc.maxZ; z++) { + for (int y = tc.minY; + y <= tc.maxY + 2; y++) { + Location loc = + new Location( + w, x, y, z); + if (loc.getBlock().getType() + != Material.CHEST) + continue; + // ★ 有NBT标记→是我们的箱子 + if (!isOurChest(loc)) + continue; + // ★ 不在记录中→残留 + if (!activeChests + .containsKey(loc)) { + String region = + getChestRegion(loc); + long time = + getChestTime(loc); + loc.getBlock().setType( + Material.AIR); + total++; + plugin.getLogger().info( + "[寻宝] 清除残留: [" + + x + "," + + y + "," + + z + "] 区域=" + + region); + } + } + } + } + } + if (total > 0) { + plugin.getLogger().info( + "[寻宝] ★清除残留: " + + total + " 个"); + } + } + + + /** + * 在Y范围内找可放箱子的位置 + * 从Y范围底部向上找: + * 找到实心方块 → 箱子放它上面 + * 上方有2格空气(箱子+开箱空间)→ 可用 + */ + private int findPlaceableY(World w, int x, int z, + int minY, int maxY) { + List validYs = new ArrayList<>(); + + // 第一轮:找实心方块上放箱子 + for (int y = minY; y <= maxY; y++) { + Block ground = w.getBlockAt(x, y, z); + Block chest = w.getBlockAt(x, y + 1, z); + Block above = w.getBlockAt(x, y + 2, z); + + if (ground.getType().isSolid() + && !chest.getType().isSolid() + && above.getType() == Material.AIR) { + validYs.add(y + 1); + } + } + + // 第二轮兜底:找空气+上方空气 + if (validYs.isEmpty()) { + for (int y = minY; y <= maxY; y++) { + Block here = w.getBlockAt(x, y, z); + Block above2 = w.getBlockAt( + x, y + 1, z); + if (here.getType() == Material.AIR + && above2.getType() + == Material.AIR) { + validYs.add(y); + } + } + } + + if (validYs.isEmpty()) return -1; + + return validYs.get( + ThreadLocalRandom.current() + .nextInt(validYs.size())); + } + + // ★ 物品中文名映射 + private static final Map ITEM_NAMES + = new HashMap<>(); + static { + ITEM_NAMES.put("DIAMOND", "§b钻石"); + ITEM_NAMES.put("DIAMOND_ORE", "§b钻石矿石"); + ITEM_NAMES.put("GOLD_INGOT", "§6金锭"); + ITEM_NAMES.put("GOLD_NUGGET", "§6金粒"); + ITEM_NAMES.put("IRON_INGOT", "§f铁锭"); + ITEM_NAMES.put("EMERALD", "§a绿宝石"); + ITEM_NAMES.put("LAPIS_LAZULI", "§9青金石"); + ITEM_NAMES.put("REDSTONE", "§4红石"); + ITEM_NAMES.put("COAL", "§8煤炭"); + ITEM_NAMES.put("NETHERITE_INGOT", "§8下界合金锭"); + ITEM_NAMES.put("NETHERITE_SCRAP", "§8下界合金碎片"); + ITEM_NAMES.put("DIAMOND_SWORD", "§b钻石剑"); + ITEM_NAMES.put("DIAMOND_PICKAXE", "§b钻石镐"); + ITEM_NAMES.put("DIAMOND_AXE", "§b钻石斧"); + ITEM_NAMES.put("DIAMOND_SHOVEL", "§b钻石铲"); + ITEM_NAMES.put("DIAMOND_HELMET", "§b钻石头盔"); + ITEM_NAMES.put("DIAMOND_CHESTPLATE", "§b钻石胸甲"); + ITEM_NAMES.put("DIAMOND_LEGGINGS", "§b钻石护腿"); + ITEM_NAMES.put("DIAMOND_BOOTS", "§b钻石靴子"); + ITEM_NAMES.put("GOLDEN_SWORD", "§6金剑"); + ITEM_NAMES.put("GOLDEN_APPLE", "§6金苹果"); + ITEM_NAMES.put("ENCHANTED_GOLDEN_APPLE", + "§6附魔金苹果"); + ITEM_NAMES.put("STICK", "§a木棍"); + ITEM_NAMES.put("BREAD", "§6面包"); + ITEM_NAMES.put("COOKED_BEEF", "§6熟牛排"); + ITEM_NAMES.put("COOKED_PORKCHOP", "§6熟猪排"); + ITEM_NAMES.put("COOKED_CHICKEN", "§6熟鸡肉"); + ITEM_NAMES.put("ARROW", "§f箭矢"); + ITEM_NAMES.put("ENDER_PEARL", "§5末影珍珠"); + ITEM_NAMES.put("BLAZE_ROD", "§6烈焰棒"); + ITEM_NAMES.put("GHAST_TEAR", "§f恶魂之泪"); + ITEM_NAMES.put("SHULKER_SHELL", "§5潜影壳"); + ITEM_NAMES.put("EXPERIENCE_BOTTLE", + "§a经验瓶"); + ITEM_NAMES.put("TOTEM_OF_UNDYING", + "§6不死图腾"); + ITEM_NAMES.put("ELYTRA", "§8鞘翅"); + ITEM_NAMES.put("TRIDENT", "§b三叉戟"); + ITEM_NAMES.put("BEACON", "§f信标"); + } + + /** 获取物品中文显示名 */ + public static String getItemDisplayName( + String materialName) { + String upper = materialName.toUpperCase(); + if (ITEM_NAMES.containsKey(upper)) { + return ITEM_NAMES.get(upper); + } + // 转为可读名:DIAMOND_ORE → Diamond Ore + String readable = materialName + .replace("_", " ") + .toLowerCase(); + StringBuilder sb = new StringBuilder(); + for (String word : readable.split(" ")) { + if (!word.isEmpty()) { + sb.append(Character + .toUpperCase(word.charAt(0))); + sb.append(word.substring(1)); + sb.append(" "); + } + } + return "§f" + sb.toString().trim(); + } + + // ★ 区域边框存储 + private final Map> regionBorders + = new HashMap<>(); + + + private void setBorderBlock(World w, int x, + int z, + Map blocks) { + int y = w.getHighestBlockYAt(x, z) + 1; + Location loc = new Location(w, x, y, z); + Block block = w.getBlockAt(loc); + // 记录原方块 + if (!blocks.containsKey(loc)) { + blocks.put(loc, block.getType()); + } + // 放置荧石 + block.setType(Material.GLOWSTONE); + } + + + + public boolean isTreasure(Location loc) { + return activeChests.containsKey(loc); + } + + public void removeChest(Location loc) { + activeChests.remove(loc); + } + + public Map getConfigs() { + return configs; + } + + public Map getActiveChests() { + return activeChests; + } + + public List getRegionNames() { + return new ArrayList<>(configs.keySet()); + } +} diff --git a/src/main/java/Sdf1_game/FileGenerator.java b/src/main/java/Sdf1_game/FileGenerator.java index 76476c8..034c891 100644 --- a/src/main/java/Sdf1_game/FileGenerator.java +++ b/src/main/java/Sdf1_game/FileGenerator.java @@ -63,6 +63,9 @@ public class FileGenerator { l.add("罚时结束提示: §a罚时已结束"); l.add("# 消息前缀"); l.add("消息前缀: §e§l【快问快答】§r"); + l.add("# 欢迎游玩草原探险服务器 "); + l.add("# 服务器ip:mc2.ypshidifu.cn"); + l.add("# 端口:30679,Java版免输入"); return l; } @@ -95,6 +98,7 @@ public class FileGenerator { l.add("| M.C的全称是什么? | Mojang Studios | 10 |"); return l; } + public static void ensureTreasureConfig( File dir) { dir.mkdirs(); @@ -106,11 +110,21 @@ public class FileGenerator { new FileOutputStream(f), StandardCharsets.UTF_8); w.write("# 寻宝全局设置\n"); + w.write("# 保底债券: 是/否\n"); w.write("保底债券: 是\n"); + w.write("# 随机奖励数量范围\n"); w.write("随机数量: 1~3\n"); + w.write("# 刷新费用(债券)\n"); + w.write("刷新费用: 150\n"); + w.write("# 刷新最低奖励数\n"); + w.write("刷新最低奖励: 3\n"); + w.write("# 刷新次数(每个宝箱)\n"); + w.write("刷新次数: 1\n"); + w.write("# 欢迎游玩草原探险服务器"); + w.write("# 服务器ip:mc2.ypshidifu.cn"); + w.write("# 端口号30679,Java免输入"); w.close(); } catch (IOException ignored) { } } - } \ No newline at end of file diff --git a/src/main/java/Sdf1_game/Main.java b/src/main/java/Sdf1_game/Main.java index c1eeb61..ed3de4d 100644 --- a/src/main/java/Sdf1_game/Main.java +++ b/src/main/java/Sdf1_game/Main.java @@ -23,6 +23,7 @@ public class Main extends JavaPlugin private QuizManager quizManager; private TreasureManager treasureManager; private BukkitTask spawnTask; + private UpdateChecker updateChecker; private static final Pattern SPEC_NUM = Pattern.compile( @@ -89,6 +90,11 @@ public class Main extends JavaPlugin treasureManager.forceCleanClaims(); treasureManager.cleanResidualChests(); } + + // ★ 启动时异步检查更新(GitHub/Gitee双通道) + updateChecker = new UpdateChecker(this); + updateChecker.checkOnEnable(); + getLogger().info("=== Sdf1_game v1.0 ==="); getLogger().info("债券桥接: " + (bondBridge.isHooked() @@ -134,7 +140,11 @@ public class Main extends JavaPlugin treasureManager.forceCleanClaims(); treasureManager.cleanResidualChests(); } - saveConfig(); + // 仅在config.yml已存在时才保存(防止自动创建空白文件) + if (new File(getDataFolder(), "config.yml").exists()) { + saveConfig(); + } + getLogger().info("\n" + " __ __ _ \n" + " \\ \\ / / | | \n" + @@ -214,6 +224,12 @@ public class Main extends JavaPlugin sender.sendMessage(bondBridge.isHooked() ? "§a已连接" : "§c未连接"); return true; + case "update": + if (updateChecker == null) { + updateChecker = new UpdateChecker(this); + } + updateChecker.checkUpdate(sender); + return true; default: sendQuizHelp(sender); return true; @@ -427,6 +443,35 @@ public class Main extends JavaPlugin case "off": case "关闭": return handleBorder(s, a); + case "public": + case "公布": + // /寻宝 public → 公布全部最旧1个 + // /寻宝 public 主城区 → 公布指定区域 + String announceRegion = null; + if (a.length >= 2) { + announceRegion = a[1]; + } + boolean ok = treasureManager + .announceChest( + announceRegion); + if (ok) { + s.sendMessage( + "§a[寻宝] 已公布宝箱坐标"); + } else { + s.sendMessage( + "§c[寻宝] 当前没有" + + (announceRegion != null + ? "该区域的" : "") + + "宝箱"); + } + return true; + case "update": + if (updateChecker == null) { + updateChecker = new UpdateChecker(this); + } + updateChecker.checkUpdate(s); + return true; + default: return handleSetName(s, a); } @@ -650,7 +695,6 @@ public class Main extends JavaPlugin p2.getBlockZ()) - ex; int maxZ = Math.max(p1.getBlockZ(), p2.getBlockZ()) + ex; - // ★ 高度范围 = A/B点Y中较小的~较大的 int y1 = p1.getBlockY(); int y2 = p2.getBlockY(); int hMin = Math.min(y1, y2); @@ -675,28 +719,6 @@ public class Main extends JavaPlugin + hMin + "~Y" + hMax); s.sendMessage("§7使用 §e/寻宝 off " + name + " §7关闭边框"); - s.sendMessage("§b§l\n欢迎游玩草原探险服务器"); - s.sendMessage("§b§l服务器ip:mc2.ypshidifu.cn\n端口30679"); - s.sendMessage(""); - } - - boolean o1k = treasureManager.saveRegion( - name, - p1.getWorld().getName(), - minX, minZ, maxX, maxZ, - hMin, hMax); - - if (ok) { - tr.reset(); - s.sendMessage("§a区域「" + name - + "」已创建!"); - s.sendMessage("§7范围: [" - + minX + "," + minZ + "] ~ [" - + maxX + "," + maxZ + "]"); - s.sendMessage("§7使用 §e/寻宝 expand " - + name + " §7扩展"); - s.sendMessage("§7使用 §e/寻宝 return " - + name + " §7收缩"); } else { s.sendMessage("§c创建失败"); } @@ -748,6 +770,11 @@ public class Main extends JavaPlugin return bondBridge; } + // ★ 供集群更新调用 + public UpdateChecker getUpdateChecker() { + return updateChecker; + } + // ========== Tab ========== @Override @@ -764,7 +791,7 @@ public class Main extends JavaPlugin String in = a[0].toLowerCase(); for (String x : Arrays.asList( "start", "stop", "reload", - "test", "info", "skip")) { + "test", "info", "skip", "update")) { if (x.startsWith(in)) r.add(x); } return r; @@ -812,7 +839,8 @@ public class Main extends JavaPlugin "info", "list", "reload", "工具", "expand", "return", "on", "off", "开启", "关闭", - "removeregion")) { + "removeregion", + "public", "公布", "update")) { if (x.startsWith(in)) r.add(x); } return r; @@ -825,7 +853,9 @@ public class Main extends JavaPlugin || sub.equals("关闭") || sub.equals("expand") || sub.equals("return") - || sub.equals("removeregion")) { + || sub.equals("removeregion") + || sub.equals("public") + || sub.equals("公布")) { String in = a[1].toLowerCase(); for (String n : treasureManager.getRegionNames()) { @@ -836,7 +866,6 @@ public class Main extends JavaPlugin } } - if (isT && a.length == 2) { String sub = a[0].toLowerCase(); if (sub.equals("expand") @@ -852,13 +881,11 @@ public class Main extends JavaPlugin } } - if (isT && a.length == 2 && a[0].equalsIgnoreCase("expand")) { - // 不补全,让玩家自己输数字 return r; } -// 打了包但还没测试。可以先测试,也可以先做return的功能要求。要先把大服务器开起来 + if (isT && a.length == 2 && a[0].equalsIgnoreCase( "removeregion")) { @@ -872,7 +899,6 @@ public class Main extends JavaPlugin } - return r; } diff --git a/src/main/java/Sdf1_game/TreasureConfig.java b/src/main/java/Sdf1_game/TreasureConfig.java index 2a65a6a..bcde89d 100644 --- a/src/main/java/Sdf1_game/TreasureConfig.java +++ b/src/main/java/Sdf1_game/TreasureConfig.java @@ -7,6 +7,8 @@ import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import org.bukkit.Location; + public class TreasureConfig { @@ -33,7 +35,10 @@ public class TreasureConfig { // ★ 中文名映射:奖励索引→显示名 public java.util.Map rewardNames = new java.util.LinkedHashMap<>(); - + // ★ 刷新相关 + public int refreshBondCost = 150; + public int refreshMinRewards = 3; + public int refreshTimes = 1; public TreasureConfig(String name) { diff --git a/src/main/java/Sdf1_game/TreasureData.java b/src/main/java/Sdf1_game/TreasureData.java new file mode 100644 index 0000000..83a4975 --- /dev/null +++ b/src/main/java/Sdf1_game/TreasureData.java @@ -0,0 +1,25 @@ +package Sdf1_game; + +import org.bukkit.Location; +import java.util.List; + +public class TreasureData { + public TreasureConfig config; + public Location chestLoc; + public int bondAmount; + public List rolled; + public boolean isRefreshed; + + public TreasureData( + TreasureConfig config, + Location chestLoc, + int bondAmount, + List rolled, + boolean isRefreshed) { + this.config = config; + this.chestLoc = chestLoc; + this.bondAmount = bondAmount; + this.rolled = rolled; + this.isRefreshed = isRefreshed; + } +} diff --git a/src/main/java/Sdf1_game/TreasureInventory.java b/src/main/java/Sdf1_game/TreasureInventory.java index db9a8d1..b7c778f 100644 --- a/src/main/java/Sdf1_game/TreasureInventory.java +++ b/src/main/java/Sdf1_game/TreasureInventory.java @@ -2,6 +2,7 @@ package Sdf1_game; import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; +import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.enchantments.Enchantment; @@ -58,16 +59,22 @@ public class TreasureInventory { } - public static void open(Player player, - TreasureConfig config) { + public static void openTreasure( + Player player, + TreasureConfig config, + Location chestLoc) { Main plugin = (Main) Bukkit .getPluginManager() .getPlugin("Sdf1_game"); + // ★ 复制奖励列表,避免共享引用 + List rewardsCopy = + new ArrayList<>(config.rewards); + // 保底债券 int bondAmount = 0; if (config.guaranteeBond) { - for (TreasureReward r : config.rewards) { + for (TreasureReward r : rewardsCopy) { if (r.getType() == TreasureReward.Type.BOND) { bondAmount = r.rollBondAmount(); @@ -76,10 +83,10 @@ public class TreasureInventory { } } - // 随机非债券奖励 + // 随机非债券 List others = new ArrayList<>(); - for (TreasureReward r : config.rewards) { + for (TreasureReward r : rewardsCopy) { if (r.getType() != TreasureReward.Type.BOND) { others.add(r); @@ -92,6 +99,7 @@ public class TreasureInventory { .nextInt( config.randomMin, config.randomMax + 1); + int totalW = 0; for (TreasureReward r : others) totalW += r.getWeight(); @@ -121,75 +129,174 @@ public class TreasureInventory { for (TreasureReward r : pool) totalW += r.getWeight(); } - // ★ 显示GUI让玩家确认 - GuiHolder gh = new GuiHolder( - config, player); - Inventory inv = Bukkit.createInventory( - gh, 27, - "§6§l═══ 寻宝 ═══ " + config.name); + } +//开GUI + showTreasureGUI(player, config, chestLoc, + bondAmount, rolled, false); + } - // 奖励预览 - List preview = - new ArrayList<>(); - preview.add(new TreasureReward( - TreasureReward.Type.BOND, null, - 1, bondAmount, null, 0, - "§6债券+" + bondAmount)); - preview.addAll(rolled); + /** 刷新后的开箱:债券进随机池,保底3个 */ + public static void openRefreshed( + Player player, + TreasureConfig config, + Location chestLoc) { + Main plugin = (Main) Bukkit + .getPluginManager() + .getPlugin("Sdf1_game"); + + // ★ 复制奖励列表 + List all = + new ArrayList<>(config.rewards); + + List rolled = + new ArrayList<>(); + int count = ThreadLocalRandom.current() + .nextInt( + config.refreshMinRewards, + config.randomMax + 1); + + int totalW = 0; + for (TreasureReward r : all) + totalW += r.getWeight(); + + if (totalW > 0 && !all.isEmpty()) { + List pool = + new ArrayList<>(all); for (int i = 0; - i < preview.size() && i < 9; i++) { - TreasureReward r = preview.get(i); - Material mat; - if (r.getType() - == TreasureReward.Type.BOND) { - mat = Material.PAPER; - } else { - mat = Material.matchMaterial( - r.getMaterialName()); - if (mat == null) - mat = Material.PAPER; + i < count && !pool.isEmpty(); i++) { + int roll = ThreadLocalRandom + .current().nextInt(totalW); + int acc = 0; + TreasureReward pick = null; + for (TreasureReward r : pool) { + acc += r.getWeight(); + if (roll < acc) { + pick = r; + break; + } } - ItemStack slot = - new ItemStack(mat, - r.getAmount()); - ItemMeta sm = slot.getItemMeta(); - if (sm != null) { - sm.displayName( - Component.text( - r.getDisplayName())); - slot.setItemMeta(sm); - } - inv.setItem(i, slot); + if (pick == null) + pick = pool.get( + pool.size() - 1); + rolled.add(pick); + pool.remove(pick); + totalW = 0; + for (TreasureReward r : pool) + totalW += r.getWeight(); } + } +//开GUI + showTreasureGUI(player, config, chestLoc, + 0, rolled, true); + } - // 装饰边框 - ItemStack filler = new ItemStack( - Material.BLACK_STAINED_GLASS_PANE); - ItemMeta fm = filler.getItemMeta(); - if (fm != null) { - fm.displayName(Component.text("")); - filler.setItemMeta(fm); - } - for (int i = 0; i < 27; i++) { - if (inv.getItem(i) == null) - inv.setItem(i, filler); - } - - gh.setInventory(inv); - player.openInventory(inv); - + // ★ 寻宝GUI点击处理(最高优先级) + @org.bukkit.event.EventHandler(priority = + org.bukkit.event.EventPriority.HIGHEST, + ignoreCancelled = true) + public void onTreasureGUI( + org.bukkit.event.inventory + .InventoryClickEvent e) { + // ★ 精确匹配标题 + String title = e.getView().getTitle(); + if (!title.contains("寻宝结果")) { + return; } - // ★ 直接发放+延迟显示结果 - final int fBond = bondAmount; - final List fRolled = - rolled; - Bukkit.getScheduler().runTaskLater(plugin, - () -> giveRewards(player, config, - fBond, fRolled), 40L); + + // ★ 非玩家背包的槽位都拦截 + int slot = e.getRawSlot(); + if (slot < 0) return; + e.setCancelled(true); + + // ★ 只处理54格的寻宝GUI + if (e.getInventory().getSize() != 54) return; + + org.bukkit.entity.Player player = + (org.bukkit.entity.Player) + e.getWhoClicked(); + Main plugin = (Main) org.bukkit.Bukkit + .getPluginManager() + .getPlugin("Sdf1_game"); + if (plugin == null) return; + + // ★ 刷新按钮(49格) + if (slot == 49) { + Sdf1_game.TreasureManager tm = + plugin.getTreasureManager(); + Sdf1_game.TreasureData td = + tm.getPlayerTreasureData( + player.getName()); + if (td == null) return; + if (td.isRefreshed) return; + + int cost = td.config.refreshBondCost; + Sdf1_game.BondBridge bb = + plugin.getBondBridge(); + int current = 0; + if (bb != null && bb.isHooked()) { + current = bb.getBonds( + player.getName()); + } + + if (current < cost) { + player.sendMessage( + "§c[寻宝] 债券不足,需要" + + cost + "张,当前" + + current + "张"); + return; + } + + // 扣债券 + if (bb != null && bb.isHooked()) { + bb.addBonds(player.getName(), + -cost, "寻宝刷新"); + } + + // 标记已刷新 + tm.markRefreshed(td.chestLoc); + + // 关闭旧GUI + player.closeInventory(); + + // 刷新后重新开箱(债券进随机池) + Sdf1_game.TreasureInventory + .openRefreshed(player, + td.config, td.chestLoc); + + player.sendMessage( + "§e[寻宝] 已刷新!消耗" + + cost + "债券"); + return; + } + + // ★ 确认领取按钮(50格) + if (slot == 50) { + Sdf1_game.TreasureManager tm = + plugin.getTreasureManager(); + Sdf1_game.TreasureData td = + tm.getPlayerTreasureData( + player.getName()); + if (td == null) return; + + player.closeInventory(); + + Sdf1_game.TreasureInventory + .giveRewards(player, + td.config, + td.bondAmount, + td.rolled); + + tm.clearPlayerTreasureData( + player.getName()); + return; + } + + // ★ 其他槽位都不处理(纯展示) } + // ★ isCustomItem public static boolean isCustomItem( ItemStack item) { @@ -197,16 +304,20 @@ public class TreasureInventory { if (!item.hasItemMeta()) return false; ItemMeta meta = item.getItemMeta(); if (meta == null) return false; - if (!meta.hasLore()) return false; List lore = meta.lore(); if (lore == null) return false; for (Component lc : lore) { - String line = net.kyori.adventure.text - .serializer.legacy - .LegacyComponentSerializer - .legacySection().serialize(lc); - if (line.contains(MARK + "CUSTOM")) + String line = + net.kyori.adventure.text + .serializer.legacy + .LegacyComponentSerializer + .legacySection() + .serialize(lc); + // ★ 同时检查两种标记格式 + if (line.contains("CUSTOM") + || line.contains("§0§k")) { return true; + } } return false; } @@ -245,50 +356,84 @@ public class TreasureInventory { ep[1].trim()); } catch (Exception ignore) { } + + plugin.getLogger().info( + "[寻宝] ★★★尝试附魔: id=" + + id + " lvl=" + lvl); + Enchantment ench = Enchantment.getByKey( NamespacedKey .minecraft(id)); + + plugin.getLogger().info( + "[寻宝] ★★★getByKey结果: " + + ench); + + if (ench == null) { + ench = Enchantment.getByName( + id.toUpperCase()); + plugin.getLogger().info( + "[寻宝] ★★★getByName结果: " + + ench); + } + if (ench != null) { - // ★ 每个附魔单独打,不共享meta + meta.removeEnchant(ench); meta.addEnchant(ench, lvl, true); + int actual = meta + .getEnchantLevel(ench); + plugin.getLogger().info( + "[寻宝] ★★★附魔成功: " + + id + " 期望=" + + lvl + " 实际=" + + actual); + } else { + plugin.getLogger().warning( + "[寻宝] ★★★附魔失败: " + + id + + " 两种方式都null"); } } - // ★ 所有附魔加完后一次性设置 - item.setItemMeta(meta); - } - String tag = "§0§kCUSTOM|" + // ★ 计算到期时间 + long expireTime = 0; + if (r.getDurationSec() > 0) { + expireTime = System.currentTimeMillis() + + (long) r.getDurationSec() * 1000; + } + + // ★ 标记格式: CUSTOM|玩家|区域|名称|上限|次数|到期时间戳 + String tag = "§0§k" + "CUSTOM|" + playerName + "|" + regionName + "|" + r.getDisplayName() + "|" - + r.getAttackUsesLimit() + "|0"; + + r.getAttackUsesLimit() + "|" + + "0|" + + expireTime; + List lore = new ArrayList<>(); if (r.getDurationSec() > 0) { - lore.add(Component.text( - "§7时限: " - + TreasureReward - .formatTime( - r.getDurationSec()))); + lore.add(Component.text("§7时限: " + + TreasureReward.formatTime( + r.getDurationSec()))); lore.add(Component.text( "§c§o到期自动收回")); } if (r.getAttackUsesLimit() > 0) { - lore.add(Component.text( - "§c攻击上限: " - + r.getAttackUsesLimit() - + "次")); + lore.add(Component.text("§c攻击上限: " + + r.getAttackUsesLimit() + "次")); } lore.add(Component.text(tag)); meta.lore(lore); item.setItemMeta(meta); return item; + } - // ===== giveRewards ===== - private static void giveRewards( + public static void giveRewards( Player player, TreasureConfig config, int bondAmount, @@ -300,18 +445,12 @@ public class TreasureInventory { TreasureManager tm = plugin.getTreasureManager(); StringBuilder msg = new StringBuilder(); - msg.append("§6§l═══ 寻宝结果 ═══\n"); - // ★ 控制台打印开箱结果 - StringBuilder log = new StringBuilder(); - log.append("[寻宝开箱] "); - log.append(player.getName()); - log.append(" | 区域=" + config.name); + msg.append("§b§l═══ 寻宝结果 ═══\n"); // 债券 if (bondAmount > 0) { int oldBond = 0; if (bb != null && bb.isHooked()) { - // ★ 先查原始余额 oldBond = bb.getBonds( player.getName()); bb.addBonds(player.getName(), @@ -323,14 +462,13 @@ public class TreasureInventory { .append(" §7→§a ") .append(newBond) .append(" §6(+" + bondAmount + ")\n"); - - msg.append("§6债券: §e+") - .append(bondAmount) - .append(" 张\n"); } + for (TreasureReward r : rolled) { if (r.getType() == TreasureReward.Type.ITEM) { + + // 限时物品领取检查 if (r.getDurationSec() > 0 && tm.isClaimed( player.getName(), @@ -338,86 +476,131 @@ public class TreasureInventory { config.name)) { msg.append("§c") .append(r.getDisplayName()) - .append(" 已领过\n"); + .append(" §7已领过\n"); continue; } - ItemStack give; - if (r.getDurationSec() > 0 - || r.getAttackUsesLimit() - > 0) { - give = buildCustomItem(r, - player.getName(), - config.name, plugin); + + Material m = resolveMaterial( + r.getMaterialName()); + if (m == null) m = Material.PAPER; + ItemStack give = + new ItemStack(m, + r.getAmount()); + ItemMeta im = give.getItemMeta(); + if (im != null) { + // 名称 + im.displayName(Component.text( + r.getDisplayName())); + + // lore + List lore = + new ArrayList<>(); if (r.getDurationSec() > 0) { - tm.addClaim( - player.getName(), - r.getDisplayName(), - config.name); + lore.add(Component.text( + "§7时限: " + + TreasureReward + .formatTime( + r.getDurationSec()))); + lore.add(Component.text( + "§c§o到期自动收回")); } - } else { - Material m = - Material.matchMaterial( - r.getMaterialName()); - if (m == null) { - m = Material.PAPER; + if (r.getAttackUsesLimit() > 0) { + lore.add(Component.text( + "§c攻击上限: " + + r.getAttackUsesLimit() + + "次")); + } + // ★ 限时物品 CUSTOM 标记 + if (r.getDurationSec() > 0 + || r.getAttackUsesLimit() + > 0) { + lore.add(Component.text( + "§0§kCUSTOM|" + + player.getName() + + "|" + + config.name + + "|" + + r.getDisplayName() + + "|" + + r.getAttackUsesLimit() + + "|0|" + + (System.currentTimeMillis() + + (long) r + .getDurationSec() + * 1000))); + } + im.lore(lore); + + // ★★★ 附魔 ★★★ + String enchStr = r.getEnchant(); + if (enchStr != null + && !enchStr.isEmpty()) { + String[] pairs = + enchStr.split(";"); + for (String pair : pairs) { + String[] ep = + pair.split(","); + if (ep.length != 2) + continue; + String id = ep[0].trim() + .toLowerCase(); + int lvl = 1; + try { + lvl = Integer.parseInt( + ep[1].trim()); + } catch (Exception ignore) { + } + + Enchantment ench = + Enchantment + .getByKey( + NamespacedKey + .minecraft( + id)); + if (ench == null) { + ench = Enchantment + .getByName( + id.toUpperCase()); + } + + if (ench != null) { + im.removeEnchant(ench); + im.addEnchant(ench, + lvl, true); + plugin.getLogger().info( + "[寻宝] ★★附魔: " + + id + "=" + + lvl); + } else { + plugin.getLogger() + .warning( + "[寻宝] ★★附魔null: " + + id); + } + } } - give = new ItemStack(m, - r.getAmount()); } + + // ★ 设置meta + give.setItemMeta(im); + + // ★★★ 只发放一次 ★★★ player.getInventory() .addItem(give); - msg.append("§f") - .append(r.getDisplayName()) - .append("\n"); + + // 限时物品记录领取限制 if (r.getDurationSec() > 0) { - final int dur = - r.getDurationSec(); - final String rn = - r.getDisplayName(); - final String pn = - player.getName(); - final String rc = - config.name; - Bukkit.getScheduler() - .runTaskLater(plugin, - () -> { - for (int i = 0; - i < player - .getInventory() - .getSize(); - i++) { - ItemStack it = - player - .getInventory() - .getItem(i); - if (it != null - && isCustomItem( - it) - && getMarkName( - it) - .equals( - rn)) { - player - .getInventory() - .setItem( - i, - null); - break; - } - } - tm.removeClaim( - pn, rn, rc); - if (player - .isOnline()) { - player - .sendMessage( - "§c[寻宝] " - + rn - + " 已到期收回"); - } - }, - dur * 20L); + tm.claim(player.getName(), + r.getDisplayName(), + config.name); } + + msg.append("§a✓ ") + .append(r.getDisplayName()) + .append("§7×") + .append(r.getAmount()) + .append("\n"); + } else if (r.getType() == TreasureReward.Type.COMMAND) { String cmd = r.getCommand() @@ -429,13 +612,53 @@ public class TreasureInventory { msg.append("§a命令已执行\n"); } } - msg.append("§6§l══════════════"); - // ★ 打印到控制台 - plugin.getLogger().info(log.toString()); + msg.append("§b§l══════════════"); player.sendMessage(msg.toString()); + msg.append("§a§l\n欢迎游玩草原探险服务器"); + msg.append("§a§l服务器ip:mc2.ypshidifu.cn\n端口号30679,Java免输入端口,基岩版输端口30679"); } + private static Material resolveMaterial( + String name) { + if (name == null) return Material.PAPER; + + // 方式1: valueOf(最可靠) + try { + return Material.valueOf( + name.toUpperCase()); + } catch (Exception ignore) { + } + + // 方式2: matchMaterial + Material m = Material.matchMaterial(name); + if (m != null) return m; + + // 方式3: 中文映射 + String[][] map = { + {"旋风棒", "BREEZE_ROD"}, + {"旋风棍", "BREEZE_ROD"}, + {"烈焰棒", "BLAZE_ROD"}, + {"钻石", "DIAMOND"}, + {"金锭", "GOLD_INGOT"}, + {"铁锭", "IRON_INGOT"}, + {"面包", "BREAD"}, + {"木棍", "STICK"}, + }; + for (String[] pair : map) { + if (name.contains(pair[0])) { + try { + return Material.valueOf(pair[1]); + } catch (Exception ignore) { + } + } + } + + return Material.PAPER; + } + + + // ★ parseMark public static String[] parseMark( ItemStack item) { @@ -445,19 +668,35 @@ public class TreasureInventory { List lore = meta.lore(); if (lore == null) return null; for (Component lc : lore) { - String line = net.kyori.adventure.text - .serializer.legacy - .LegacyComponentSerializer - .legacySection().serialize(lc); - if (line.contains(MARK + "CUSTOM")) { - String data = line.substring( - line.indexOf(MARK + "CUSTOM") - + MARK.length()); + String line = + net.kyori.adventure.text + .serializer.legacy + .LegacyComponentSerializer + .legacySection() + .serialize(lc); + if (line.contains("§0§kCUSTOM")) { + int idx = line.indexOf("§0§kCUSTOM"); + String data = + line.substring(idx + 3); String[] p = data.split("\\|"); - if (p.length >= 6) + // ★ 至少7个字段 + if (p.length >= 7) { + return new String[]{ + p[1], // 玩家 + p[2], // 区域 + p[3], // 名称 + p[4], // 上限 + p[5], // 次数 + p[6] // 到期时间戳 + }; + } + // 兼容旧格式(6个字段) + if (p.length >= 6) { return new String[]{ p[1], p[2], p[3], - p[4], p[5]}; + p[4], p[5], "0" + }; + } } } return null; @@ -602,76 +841,262 @@ public class TreasureInventory { // ===== 点击处理 ===== + /** GUI点击处理 */ public static boolean handleClick( InventoryClickEvent event, BondBridge bb, Main plugin) { - InventoryHolder holder = - event.getInventory().getHolder(); - if (!(holder instanceof GuiHolder)) - return false; + if (event.getView().getTitle() + .contains("寻宝结果")) { + event.setCancelled(true); - event.setCancelled(true); - if (event.getRawSlot() < 0 - || event.getRawSlot() >= 27) - return true; + int slot = event.getRawSlot(); + Player player = + (Player) event.getWhoClicked(); + TreasureManager tm = + plugin.getTreasureManager(); + TreasureData td = + tm.getPlayerTreasureData( + player.getName()); + if (td == null) return true; - ItemStack clicked = event.getCurrentItem(); - if (clicked == null) return true; - if (clicked.getType() - == Material.BLACK_STAINED_GLASS_PANE) - return true; - - GuiHolder gh = (GuiHolder) holder; - Player player = gh.getPlayer(); - TreasureConfig config = gh.getConfig(); - - // ★ 从lore中解析索引标记 - ItemMeta im = clicked.getItemMeta(); - if (im == null || !im.hasLore()) return true; - - int rewardIndex = -1; - for (Component lc : im.lore()) { - String line = net.kyori.adventure.text - .serializer.legacy - .LegacyComponentSerializer - .legacySection().serialize(lc); - if (line.startsWith(LORE_INDEX_PREFIX)) { - // 格式: §0§k0,BOND,50 - String data = line.substring( - LORE_INDEX_PREFIX.length()); - String[] parts = data.split(","); - try { - rewardIndex = - Integer.parseInt(parts[0]); - } catch (Exception ignored) { + // ★ 刷新按钮(第49格) + if (slot == 49 && !td.isRefreshed) { + // 检查债券 + int cost = td.config + .refreshBondCost; + int current = 0; + if (bb != null && bb.isHooked()) { + current = bb.getBonds( + player.getName()); } - break; + if (current < cost) { + player.sendMessage( + "§c[寻宝] 债券不足,需要" + + cost + "张"); + return true; + } + + // 扣债券 + if (bb != null && bb.isHooked()) { + bb.addBonds(player.getName(), + -cost, "寻宝刷新"); + } + + // ★ 标记已刷新 + tm.markRefreshed(td.chestLoc); + + // ★ 刷新后开箱(债券进随机池) + openRefreshed(player, td.config, + td.chestLoc); + + player.sendMessage( + "§e[寻宝] 已刷新!消耗" + + cost + "债券"); + return true; + } + + // ★ 确认领取按钮(第50格) + if (slot == 50) { + player.closeInventory(); + giveRewards(player, td.config, + td.bondAmount, + td.rolled); + tm.clearPlayerTreasureData( + player.getName()); + return true; } - } - if (rewardIndex < 0 - || rewardIndex >= config.rewards.size()) return true; - - TreasureReward matched = - config.rewards.get(rewardIndex); - - player.closeInventory(); - open(player, config); - - - // 领取后删除GUI物品 - event.getInventory() - .setItem(event.getRawSlot(), null); - - player.sendMessage("§a§l[寻宝]§r 领取了 §6" - + matched.getDisplayName() + "§r!"); - return true; + } + return false; } + /** 显示寻宝结果GUI */ + private static void showTreasureGUI( + Player player, + TreasureConfig config, + Location chestLoc, + int bondAmount, + List rolled, + boolean isRefreshed) { + + Main plugin = (Main) Bukkit + .getPluginManager() + .getPlugin("Sdf1_game"); + + Inventory inv = Bukkit.createInventory( + null, 54, + "§b§l═══ 寻宝结果 ═══"); + + int slot = 0; + + // ★ 债券(如果有保底) + if (bondAmount > 0) { + ItemStack bondItem = + new ItemStack(Material.PAPER); + ItemMeta bm = bondItem.getItemMeta(); + if (bm != null) { + bm.displayName(Component.text( + "§6保底债券 §ex" + bondAmount)); + List bl = new ArrayList<>(); + bl.add(Component.text( + "§7开箱后自动到账")); + bm.lore(bl); + bondItem.setItemMeta(bm); + } + inv.setItem(slot, bondItem); + slot++; + } + + // ★ 奖励物品 + for (TreasureReward r : rolled) { + if (slot >= 45) break; + + Material mat = Material.PAPER; + + if (r.getType() + == TreasureReward.Type.BOND) { + mat = Material.PAPER; + } else if (r.getMaterialName() != null) { + // 方式1: matchMaterial + mat = Material.matchMaterial( + r.getMaterialName()); + // 方式2: valueOf + if (mat == null) { + try { + mat = Material.valueOf( + r.getMaterialName()); + } catch (Exception ignored) { + } + } + // 方式3: 大写再试 + if (mat == null) { + try { + mat = Material.valueOf( + r.getMaterialName() + .toUpperCase()); + } catch (Exception ignored) { + } + } + if (mat == null) { + plugin.getLogger().warning( + "[寻宝] ★材质未知: " + + r.getMaterialName()); + mat = Material.PAPER; + } + } + ItemStack item = + new ItemStack(mat, + r.getAmount()); + ItemMeta im = item.getItemMeta(); + if (im != null) { + im.displayName(Component.text( + r.getDisplayName())); + List il = + new ArrayList<>(); + if (r.getDurationSec() > 0) { + il.add(Component.text( + "§7时限: " + + TreasureReward + .formatTime( + r.getDurationSec()))); + } + if (r.getAttackUsesLimit() > 0) { + il.add(Component.text( + "§7攻击上限: " + + r.getAttackUsesLimit() + + "次")); + } + im.lore(il); + item.setItemMeta(im); + } + inv.setItem(slot, item); + slot++; + } + + // ★ 刷新按钮(第50格) + if (!isRefreshed) { + ItemStack refreshBtn = + new ItemStack( + Material.ENCHANTED_GOLDEN_APPLE); + ItemMeta rb = refreshBtn.getItemMeta(); + if (rb != null) { + rb.displayName(Component.text( + "§e§l刷新奖励 §c(150债券)")); + List rl = + new ArrayList<>(); + rl.add(Component.text( + "§7刷新后取消保底")); + rl.add(Component.text( + "§7债券也加入随机池")); + rl.add(Component.text( + "§7保底3个以上奖励")); + rl.add(Component.text( + "§c每个宝箱只能刷新1次")); + rb.lore(rl); + refreshBtn.setItemMeta(rb); + } + inv.setItem(49, refreshBtn); + } else { + // ★ 已刷新标记 + ItemStack doneBtn = + new ItemStack( + Material.RED_STAINED_GLASS_PANE); + ItemMeta db = doneBtn.getItemMeta(); + if (db != null) { + db.displayName(Component.text( + "§c已刷新(不可再刷)")); + doneBtn.setItemMeta(db); + } + inv.setItem(49, doneBtn); + } + + // ★ 确认领取按钮(第51格) + ItemStack confirmBtn = + new ItemStack(Material.CHEST); + ItemMeta cb = confirmBtn.getItemMeta(); + if (cb != null) { + cb.displayName(Component.text( + "§a§l确认领取")); + List cl = + new ArrayList<>(); + cl.add(Component.text( + "§7点击领取所有奖励")); + cb.lore(cl); + confirmBtn.setItemMeta(cb); + } + inv.setItem(50, confirmBtn); + + // ★ 装饰边框 + ItemStack filler = + new ItemStack( + Material.GRAY_STAINED_GLASS_PANE); + ItemMeta fm = filler.getItemMeta(); + if (fm != null) { + fm.displayName(Component.text("")); + filler.setItemMeta(fm); + } + for (int i = 45; i < 54; i++) { + if (inv.getItem(i) == null) + inv.setItem(i, filler); + } + + // ★ 保存数据到玩家的临时Map + plugin.getTreasureManager() + .setPlayerTreasureData( + player.getName(), + new TreasureData( + config, chestLoc, + bondAmount, + rolled, + isRefreshed)); + + player.openInventory(inv); + } + // ===== 权重随机 ===== private static List rollRewards( diff --git a/src/main/java/Sdf1_game/TreasureListener.java b/src/main/java/Sdf1_game/TreasureListener.java index 345cff3..30bf75f 100644 --- a/src/main/java/Sdf1_game/TreasureListener.java +++ b/src/main/java/Sdf1_game/TreasureListener.java @@ -1,6 +1,7 @@ package Sdf1_game; import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.block.Block; @@ -9,7 +10,9 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.block.Action; import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryType; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.ItemStack; @@ -130,61 +133,40 @@ public class TreasureListener implements Listener { } - // ★ 禁止丢弃寻宝物品 + // ★ 丢弃拦截 @org.bukkit.event.EventHandler public void onDrop( org.bukkit.event.player .PlayerDropItemEvent e) { - if (TreasureInventory - .isCustomItem( - e.getItemDrop().getItemStack())) { + if (TreasureInventory.isCustomItem( + e.getItemDrop().getItemStack())) { e.setCancelled(true); e.getPlayer().sendMessage( "§c[寻宝] 寻宝物品不可丢弃"); } } - // ★ 死亡时回收寻宝物品(不掉落) + // ★ 死亡清除 @org.bukkit.event.EventHandler public void onDeath( org.bukkit.event.entity .PlayerDeathEvent e) { org.bukkit.entity.Player player = e.getEntity(); - List drops = e.getDrops(); - - // ★ 检查背包里所有物品 - ItemStack[] contents = - player.getInventory().getContents(); for (int i = 0; - i < contents.length; i++) { - ItemStack item = contents[i]; + i < player.getInventory() + .getSize(); i++) { + ItemStack item = + player.getInventory() + .getItem(i); if (item == null) continue; if (!TreasureInventory .isCustomItem(item)) continue; - - String[] mark = - TreasureInventory.parseMark(item); - if (mark != null) { - // 从掉落列表移除 - drops.remove(item); - // 清除记录 - plugin.getTreasureManager() - .removeClaim(mark[0], - mark[2], mark[1]); - } - } - // 清空背包里所有寻宝物品 - for (int i = 0; - i < contents.length; i++) { - ItemStack item = contents[i]; - if (item == null) continue; - if (TreasureInventory - .isCustomItem(item)) { - player.getInventory() - .setItem(i, null); - } + e.getDrops().remove(item); + player.getInventory() + .setItem(i, null); } } + // ★ 物品掉落到地上时检测(丢弃后立即清除) @org.bukkit.event.EventHandler public void onItemSpawn( @@ -320,7 +302,10 @@ public class TreasureListener implements Listener { TreasureConfig tc = manager.getConfigs().get(regionName); if (tc != null) { - TreasureInventory.open(p, tc); + TreasureInventory.openTreasure( + p, tc, loc); + + } } } @@ -334,6 +319,84 @@ public class TreasureListener implements Listener { .getPendingReclaims().add(name); } + // ★ 寻宝GUI点击处理 + @org.bukkit.event.EventHandler(priority = + org.bukkit.event.EventPriority.HIGHEST, + ignoreCancelled = true) + public void onTreasureGUI( + org.bukkit.event.inventory + .InventoryClickEvent e) { + String title = e.getView().getTitle(); + if (!title.contains("寻宝结果")) return; + + int slot = e.getRawSlot(); + if (slot < 0) return; + e.setCancelled(true); + if (e.getInventory().getSize() != 54) return; + + org.bukkit.entity.Player player = + (org.bukkit.entity.Player) + e.getWhoClicked(); + Main plugin = (Main) org.bukkit.Bukkit + .getPluginManager() + .getPlugin("Sdf1_game"); + if (plugin == null) return; + + TreasureManager tm = + plugin.getTreasureManager(); + TreasureData td = + tm.getPlayerTreasureData( + player.getName()); + if (td == null) return; + + // ★ 刷新按钮(49格) + if (slot == 49 && !td.isRefreshed) { + int cost = td.config.refreshBondCost; + BondBridge bb = + plugin.getBondBridge(); + int current = 0; + if (bb != null && bb.isHooked()) { + current = bb.getBonds( + player.getName()); + } + if (current < cost) { + player.sendMessage( + "§c[寻宝] 债券不足,需要" + + cost + + "张,当前" + + current + "张"); + return; + } + if (bb != null && bb.isHooked()) { + bb.addBonds(player.getName(), + -cost, "寻宝刷新"); + } + tm.markRefreshed(td.chestLoc); + player.closeInventory(); + TreasureInventory.openRefreshed( + player, td.config, + td.chestLoc); + player.sendMessage( + "§e[寻宝] 已刷新!消耗" + + cost + "债券"); + return; + } + + // ★ 确认领取按钮(50格) + // 确认领取(50格) + if (slot == 50) { + player.closeInventory(); + // ★ 只调用一次 + TreasureInventory.giveRewards( + player, td.config, + td.bondAmount, td.rolled); + tm.clearPlayerTreasureData( + player.getName()); + return; + } + + // ★ 其他槽位全部取消(只展示) + } // ===== 阻止破坏宝箱 ===== @@ -362,53 +425,247 @@ public class TreasureListener implements Listener { } @org.bukkit.event.EventHandler(priority = - org.bukkit.event.EventPriority.HIGH) - public void onClick( + org.bukkit.event.EventPriority.HIGHEST) + public void onInvClick( org.bukkit.event.inventory .InventoryClickEvent e) { - if (e.getCursor() == null) return; - if (!TreasureInventory - .isCustomItem(e.getCursor())) - return; - String type = e.getView() - .getTopInventory().getType().name(); - if (!type.equals("PLAYER") - && !type.equals("CRAFTING") - && !type.equals("CREATIVE")) { + if (e.getWhoClicked() == null) return; + org.bukkit.entity.Player p = + (org.bukkit.entity.Player) + e.getWhoClicked(); + Main plugin = (Main) Bukkit + .getPluginManager() + .getPlugin("Sdf1_game"); + String title = e.getView().getTitle(); + + // ========== 1. 寻宝GUI ========== + if (title.contains("寻宝结果")) { e.setCancelled(true); - if (e.getWhoClicked() - instanceof org.bukkit.entity.Player) { - ((org.bukkit.entity.Player) - e.getWhoClicked()) - .sendMessage( - "§c[寻宝] 不可放入容器"); + int slot = e.getRawSlot(); + if (slot < 0) return; + if (e.getInventory().getSize() != 54) + return; + handleTreasureGUI(p, slot, e); + return; + } + + // ========== 2. 检测自定义物品 ========== + ItemStack checkItem = null; + + // 光标有物品(普通点击) + if (e.getCursor() != null + && e.getCursor().getType() + != Material.AIR) { + checkItem = e.getCursor(); + } + + // shift-click + if (checkItem == null + && e.isShiftClick() + && e.getCurrentItem() != null + && e.getCurrentItem().getType() + != Material.AIR) { + checkItem = e.getCurrentItem(); + } + + // ========== 3. 非自定义物品直接放行 ========== + if (checkItem == null) return; + if (!TreasureInventory + .isCustomItem(checkItem)) return; + + // ========== 4. 确认是容器 ========== + InventoryType topType = e.getView() + .getTopInventory().getType(); + // 只放行玩家背包和创造模式背包,其他所有容器都拦截 + if (topType == InventoryType.PLAYER + || topType == InventoryType.CREATIVE) { + return; + } + + // ========== 5. 自定义物品+容器 → 拦截 ========== + e.setCancelled(true); + + plugin.getLogger().info( + "[寻宝] ★★★拦截: " + p.getName() + + " 物品=" + checkItem.getType() + + " 容器=" + topType); + + // 非本人→直接没收 + String[] mark = TreasureInventory + .parseMark(checkItem); + if (mark != null + && !mark[0].equalsIgnoreCase( + p.getName())) { + checkItem.setAmount(0); + p.getOpenInventory().setCursor(null); + p.sendMessage( + "§c[寻宝] 该物品不属于你,已没收"); + return; + } + + // ★ 事不过三 + boolean confiscate = plugin + .getTreasureManager() + .onContainerBlock(p.getName()); + + if (confiscate) { + // 第3次:没收 + String itemName = mark != null + ? mark[2] : "?"; + String region = mark != null + ? mark[1] : "?"; + + checkItem.setAmount(0); + p.getOpenInventory().setCursor(null); + + plugin.getTreasureManager() + .unclaim(p.getName(), + itemName, region); + + p.sendMessage( + "§c[寻宝] 多次违规," + + itemName + " 已被没收!"); + } else { + // 第1-2次:退回 + if (p.getOpenInventory() + .getCursor() != null + && p.getOpenInventory() + .getCursor().getType() + != Material.AIR) { + ItemStack cur = p.getOpenInventory() + .getCursor(); + p.getOpenInventory().setCursor(null); + p.getInventory().addItem(cur); } + int count = plugin + .getTreasureManager() + .getContainerBlockCount( + p.getName()); + p.sendMessage( + "§c[寻宝] 不可放入容器!" + + "第" + count + "次警告(" + + (3 - count) + + "次后没收)"); } } + // ★ 寻宝GUI处理 + private void handleTreasureGUI( + org.bukkit.entity.Player player, + int slot, + org.bukkit.event.inventory + .InventoryClickEvent e) { + Main plugin = (Main) org.bukkit.Bukkit + .getPluginManager() + .getPlugin("Sdf1_game"); + if (plugin == null) return; + TreasureManager tm = + plugin.getTreasureManager(); + TreasureData td = + tm.getPlayerTreasureData( + player.getName()); + if (td == null) return; + + // 刷新按钮(49格) + if (slot == 49 && !td.isRefreshed) { + int cost = td.config.refreshBondCost; + BondBridge bb = + plugin.getBondBridge(); + int current = 0; + if (bb != null && bb.isHooked()) { + current = bb.getBonds( + player.getName()); + } + if (current < cost) { + player.sendMessage( + "§c[寻宝] 债券不足,需要" + + cost + "张"); + return; + } + if (bb != null && bb.isHooked()) { + bb.addBonds(player.getName(), + -cost, "寻宝刷新"); + } + tm.markRefreshed(td.chestLoc); + player.closeInventory(); + TreasureInventory.openRefreshed( + player, td.config, + td.chestLoc); + player.sendMessage( + "§e[寻宝] 已刷新!消耗" + + cost + "债券"); + return; + } + + // 确认领取(50格) + if (slot == 50) { + player.closeInventory(); + TreasureInventory.giveRewards( + player, td.config, + td.bondAmount, td.rolled); + tm.clearPlayerTreasureData( + player.getName()); + return; + } + } + + // ★ 拖拽拦截 @org.bukkit.event.EventHandler(priority = - org.bukkit.event.EventPriority.HIGH) + org.bukkit.event.EventPriority.HIGHEST) public void onDrag( org.bukkit.event.inventory .InventoryDragEvent e) { - for (ItemStack item : - e.getNewItems().values()) { - if (TreasureInventory - .isCustomItem(item)) { - String type = e.getView() - .getTopInventory() - .getType().name(); - if (!type.equals("PLAYER") - && !type.equals("CRAFTING")) { + String title = e.getView().getTitle(); + + // ★ 垃圾站拖拽拦截 + if (title.contains("垃圾回收站")) { + for (ItemStack item : + e.getNewItems().values()) { + if (item != null + && TreasureInventory + .isCustomItem(item)) { e.setCancelled(true); + Main plugin = (Main) Bukkit + .getPluginManager() + .getPlugin("Sdf1_game"); + if (plugin != null) { + plugin.getTreasureManager() + .onContainerBlock( + e.getWhoClicked() + .getName()); + } + e.getWhoClicked().sendMessage( + "§c[寻宝] 不可拖入垃圾站"); + return; + } + } + return; + } + + // ★ 通用容器拖拽拦截 + InventoryType type = e.getView() + .getTopInventory().getType(); + // 只放行玩家背包和创造模式背包,其他所有容器都拦截 + if (type != InventoryType.PLAYER + && type != InventoryType.CREATIVE) { + for (ItemStack item : + e.getNewItems().values()) { + if (item != null + && TreasureInventory + .isCustomItem(item)) { + e.setCancelled(true); + e.getWhoClicked().sendMessage( + "§c[寻宝] 不可放入容器"); return; } } } } + + // ★ 铁砧拦截 @org.bukkit.event.EventHandler(priority = - org.bukkit.event.EventPriority.HIGH) + org.bukkit.event.EventPriority.HIGHEST) public void onAnvil( org.bukkit.event.inventory .PrepareAnvilEvent e) { @@ -421,32 +678,54 @@ public class TreasureListener implements Listener { } } + + // ★ 右键容器拦截(HIGHEST 优先级) @org.bukkit.event.EventHandler(priority = - org.bukkit.event.EventPriority.HIGH) + org.bukkit.event.EventPriority.HIGHEST) public void onInteract( org.bukkit.event.player .PlayerInteractEvent e) { - if (e.getItem() == null) return; + // ★ 双重获取物品:e.getItem() + 主手后备 + ItemStack held = e.getItem(); + if (held == null || held.getType() == org.bukkit.Material.AIR) { + held = e.getPlayer().getInventory() + .getItemInMainHand(); + } + if (held == null || held.getType() == org.bukkit.Material.AIR) return; if (!TreasureInventory - .isCustomItem(e.getItem())) return; + .isCustomItem(held)) return; if (!e.getAction().name() .contains("RIGHT_CLICK")) return; if (e.getClickedBlock() == null) return; - String b = e.getClickedBlock() + String bt = e.getClickedBlock() .getType().name(); - if (b.contains("SHULKER") - || b.contains("CHEST") - || b.equals("BARREL") - || b.equals("ENDER_CHEST") - || b.equals("HOPPER") - || b.equals("DROPPER") - || b.equals("DISPENSER")) { + if (bt.contains("SHULKER") + || bt.contains("CHEST") + || bt.equals("BARREL") + || bt.equals("ENDER_CHEST") + || bt.equals("HOPPER") + || bt.equals("DROPPER") + || bt.equals("DISPENSER") + || bt.equals("DECORATED_POT") + || bt.contains("SHELF")) { e.setCancelled(true); + e.setUseItemInHand( + org.bukkit.event.Event.Result.DENY); e.getPlayer().sendMessage( "§c[寻宝] 不可放入容器"); } } - + // ★ 方块放置拦截 - 防止手持宝箱物品放置方块 + @org.bukkit.event.EventHandler(priority = + org.bukkit.event.EventPriority.HIGHEST) + public void onBlockPlace(BlockPlaceEvent e) { + ItemStack held = e.getItemInHand(); + if (held == null || held.getType() == Material.AIR) return; + if (!TreasureInventory.isCustomItem(held)) return; + e.setCancelled(true); + e.getPlayer().sendMessage( + "§c[寻宝] 不可使用此物品放置方块"); + } // ★ 上线时扫描背包回收 @org.bukkit.event.EventHandler public void onJoin( @@ -524,4 +803,119 @@ public class TreasureListener implements Listener { p.sendMessage("§7输入 §e/寻宝 区域名 " + "§7保存"); } + + // ★ 传送门拦截 - 物品通过末地传送门/下界传送门时自动销毁 + @org.bukkit.event.EventHandler(priority = + org.bukkit.event.EventPriority.HIGHEST) + public void onPortal(org.bukkit.event.player.PlayerPortalEvent e) { + Player p = e.getPlayer(); + // 检查玩家背包中的宝箱物品 + for (ItemStack item : p.getInventory().getContents()) { + if (item != null + && TreasureInventory.isCustomItem(item)) { + // 销毁宝箱物品 + item.setAmount(0); + p.sendMessage("§c[寻宝] 宝箱物品在传送时已自动销毁"); + } + } + // 检查副手 + ItemStack offhand = p.getInventory().getItemInOffHand(); + if (offhand != null + && TreasureInventory.isCustomItem(offhand)) { + offhand.setAmount(0); + p.sendMessage("§c[寻宝] 宝箱物品在传送时已自动销毁"); + } + } + + // ★ 实体传送拦截 - 驴箱子、运输船等实体传送 + @org.bukkit.event.EventHandler(priority = + org.bukkit.event.EventPriority.HIGHEST) + public void onEntityPortal(org.bukkit.event.entity.EntityPortalEvent e) { + // 检查实体是否携带宝箱物品(如驴箱子、运输船) + if (e.getEntity() instanceof org.bukkit.entity.AnimalTamer) { + // 这里可以添加更多实体类型的检查 + } + + // 检查驴箱子、骡子、羊驼等可骑乘实体 + if (e.getEntity() instanceof org.bukkit.entity.AbstractHorse) { + org.bukkit.entity.AbstractHorse horse = (org.bukkit.entity.AbstractHorse) e.getEntity(); + // 检查马鞍和装备 + ItemStack saddle = horse.getInventory().getItem(0); + if (saddle != null && TreasureInventory.isCustomItem(saddle)) { + saddle.setAmount(0); + // 通知附近的玩家 + for (Player p : horse.getWorld().getPlayers()) { + if (p.getLocation().distance(horse.getLocation()) < 32) { + p.sendMessage("§c[寻宝] 驴箱子中的宝箱物品在传送时已自动销毁"); + } + } + } + } + + // 检查运输船(如果有相关API) + // 注意:Bukkit API可能不直接支持运输船的物品检查 + } + + // ★ 漏斗拦截 - 防止宝箱物品通过漏斗传输 + @org.bukkit.event.EventHandler(priority = + org.bukkit.event.EventPriority.HIGHEST) + public void onInventoryMoveItem(org.bukkit.event.inventory.InventoryMoveItemEvent e) { + ItemStack item = e.getItem(); + if (item != null && TreasureInventory.isCustomItem(item)) { + // 检查目标容器类型 + InventoryType destinationType = e.getDestination().getType(); + // 只放行玩家背包,其他所有容器都拦截 + if (destinationType != InventoryType.PLAYER + && destinationType != InventoryType.CREATIVE) { + e.setCancelled(true); + // 将物品弹出到世界 + Location loc = e.getSource().getLocation(); + if (loc != null) { + loc.getWorld().dropItemNaturally(loc, item.clone()); + item.setAmount(0); + } + } + } + } + + // ★ 展示框拦截 - 防止宝箱物品放入展示框 + @org.bukkit.event.EventHandler(priority = + org.bukkit.event.EventPriority.HIGHEST) + public void onPlayerInteractEntity(org.bukkit.event.player.PlayerInteractEntityEvent e) { + if (!(e.getRightClicked() instanceof org.bukkit.entity.ItemFrame)) return; + Player p = e.getPlayer(); + ItemStack item = p.getInventory().getItemInMainHand(); + if (item != null && TreasureInventory.isCustomItem(item)) { + e.setCancelled(true); + p.sendMessage("§c[寻宝] 宝箱物品不可放入展示框"); + } + } + + // ★ 打开容器时检查 - 防止宝箱物品通过其他方式进入容器 + @org.bukkit.event.EventHandler(priority = + org.bukkit.event.EventPriority.HIGHEST) + public void onInventoryOpen(org.bukkit.event.inventory.InventoryOpenEvent e) { + if (!(e.getPlayer() instanceof Player)) return; + Player p = (Player) e.getPlayer(); + InventoryType type = e.getInventory().getType(); + + // 只检查容器类型,不检查玩家背包 + if (type == InventoryType.PLAYER + || type == InventoryType.CREATIVE) { + return; + } + + // 检查容器中是否已有宝箱物品 + for (ItemStack item : e.getInventory().getContents()) { + if (item != null && TreasureInventory.isCustomItem(item)) { + // 移除宝箱物品并弹出到世界 + Location loc = e.getInventory().getLocation(); + if (loc != null) { + loc.getWorld().dropItemNaturally(loc, item.clone()); + } + item.setAmount(0); + p.sendMessage("§c[寻宝] 容器中的宝箱物品已被清除"); + } + } + } } diff --git a/src/main/java/Sdf1_game/TreasureManager.java b/src/main/java/Sdf1_game/TreasureManager.java index 2421ea2..2838d26 100644 --- a/src/main/java/Sdf1_game/TreasureManager.java +++ b/src/main/java/Sdf1_game/TreasureManager.java @@ -1,13 +1,10 @@ package Sdf1_game; -import net.kyori.adventure.text.Component; import org.bukkit.*; import org.bukkit.block.Block; import org.bukkit.entity.Player; import org.bukkit.event.inventory.InventoryType; -import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.scheduler.BukkitTask; @@ -15,20 +12,74 @@ import java.io.*; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.ThreadLocalRandom; -import org.bukkit.block.Sign; -import org.bukkit.block.sign.Side; -import org.bukkit.block.BlockFace; -import org.bukkit.block.BlockState; - -import static Sdf1_game.TreasureInventory.isCustomItem; -import static Sdf1_game.TreasureInventory.parseMark; - public class TreasureManager { private static final String CONFIG_SUFFIX = ".sdf1.txt"; + // ===== 模糊物品匹配 ===== + private final java.util.Map + ITEM_CN = new java.util.HashMap<>(); + + private void initItemMap() { + ITEM_CN.put("旋风棒", "BREEZE_ROD"); + ITEM_CN.put("旋风棍", "BREEZE_ROD"); + ITEM_CN.put("旋风杖", "BREEZE_ROD"); + ITEM_CN.put("烈焰棒", "BLAZE_ROD"); + ITEM_CN.put("烈焰棍", "BLAZE_ROD"); + ITEM_CN.put("金苹果", "GOLDEN_APPLE"); + ITEM_CN.put("附魔金苹果", + "ENCHANTED_GOLDEN_APPLE"); + ITEM_CN.put("下界合金剑", + "NETHERITE_SWORD"); + ITEM_CN.put("下界合金镐", + "NETHERITE_PICKAXE"); + ITEM_CN.put("下界合金锭", + "NETHERITE_INGOT"); + ITEM_CN.put("钻石剑", "DIAMOND_SWORD"); + ITEM_CN.put("钻石镐", + "DIAMOND_PICKAXE"); + ITEM_CN.put("钻石斧", "DIAMOND_AXE"); + ITEM_CN.put("钻石甲", + "DIAMOND_CHESTPLATE"); + ITEM_CN.put("钻石靴子", + "DIAMOND_BOOTS"); + ITEM_CN.put("钻石护腿", + "DIAMOND_LEGGINGS"); + ITEM_CN.put("钻石头盔", + "DIAMOND_HELMET"); + ITEM_CN.put("末影珍珠", + "ENDER_PEARL"); + ITEM_CN.put("绿宝石", "EMERALD"); + ITEM_CN.put("金锭", "GOLD_INGOT"); + ITEM_CN.put("铁剑", "IRON_SWORD"); + ITEM_CN.put("铁镐", "IRON_PICKAXE"); + ITEM_CN.put("铁锭", "IRON_INGOT"); + ITEM_CN.put("钻石", "DIAMOND"); + ITEM_CN.put("木棍", "STICK"); + ITEM_CN.put("面包", "BREAD"); + ITEM_CN.put("弓", "BOW"); + ITEM_CN.put("箭", "ARROW"); + ITEM_CN.put("盾牌", "SHIELD"); + ITEM_CN.put("不死图腾", + "TOTEM_OF_UNDYING"); + ITEM_CN.put("煤炭", "COAL"); + ITEM_CN.put("红石", "REDSTONE"); + ITEM_CN.put("附魔书", + "ENCHANTED_BOOK"); + } + + // ===== 领取限制 ===== + private final java.util.Map + claimDb = new java.util.HashMap<>(); + private File claimFile; + + // ===== 容器拦截计数 ===== + private final java.util.Map + containerBlockCount = + new java.util.HashMap<>(); + private final Main plugin; private final Map @@ -38,6 +89,9 @@ public class TreasureManager { private static final String MARKER = "SDF1"; private final Set pendingReclaims = new HashSet<>(); + private final java.util.Map + ITEM_CN_TO_ID = new java.util.HashMap<>(); + public Set getPendingReclaims() { return pendingReclaims; @@ -45,7 +99,6 @@ public class TreasureManager { // ★ 领取记录 - private final File claimFile; private final Set claimedSet = new HashSet<>(); @@ -53,7 +106,34 @@ public class TreasureManager { private BukkitTask borderTask; private boolean borderShowing = false; private String borderRegionName; + // ★ 容器拦截计数: 玩家名 → 次数 + /** 容器拦截:第1-2次警告,第3次没收 */ + public boolean onContainerBlock( + String playerName) { + int count = containerBlockCount + .getOrDefault(playerName, 0) + 1; + containerBlockCount.put(playerName, count); + + plugin.getLogger().info( + "[寻宝] ★★★onContainerBlock: " + + playerName + + " 次数=" + count); + + if (count < 3) { + return false; + } else { + // ★ 重置计数 + containerBlockCount.put(playerName, 0); + return true; + } + } + + /** 重置容器拦截计数 */ + public void resetContainerBlock( + String playerName) { + containerBlockCount.remove(playerName); + } public TreasureManager(Main plugin) { @@ -65,6 +145,7 @@ public class TreasureManager { claimFile.getParentFile().mkdirs(); loadClaims(); // 构造器里加(最后一行): + // ★ 每2秒扫描(全场景覆盖) new org.bukkit.scheduler.BukkitRunnable() { @Override public void run() { @@ -74,10 +155,58 @@ public class TreasureManager { reclaimPlayerItems(p.getName()); } } - }.runTaskTimer(plugin, 600L, 600L); + }.runTaskTimer(plugin, 20L, 20L); // 1秒 + initItemMap(); + loadClaims(); + } + /** 模糊匹配物品名→材质ID */ + private String fuzzyMatchItem(String input) { + if (input == null || input.isEmpty()) + return null; + + // 精确匹配 + for (java.util.Map.Entry e + : ITEM_CN.entrySet()) { + if (input.equals(e.getKey())) + return e.getValue(); + } + + // 包含匹配 + String found = null; + for (java.util.Map.Entry e + : ITEM_CN.entrySet()) { + if (input.contains(e.getKey()) + || e.getKey().contains(input)) { + if (found != null) return null; + found = e.getValue(); + } + } + if (found != null) return found; + + // 前缀匹配 + if (input.length() >= 2) { + String p = input.substring(0, 2); + found = null; + for (java.util.Map.Entry e + : ITEM_CN.entrySet()) { + String k = e.getKey(); + if (k.length() >= 2 + && k.substring(0, 2) + .equals(p)) { + if (found != null) return null; + found = e.getValue(); + } + } + if (found != null) return found; + } + + return null; + } + + /** 加载领取记录 */ private void loadClaims() { claimedSet.clear(); @@ -102,21 +231,75 @@ public class TreasureManager { } } public void reload() { - forceCleanClaims(); + // ★ 先清内存 + claimDb.clear(); + saveClaims(); // 写空文件 + + // 清除玩家背包里的自定义物品 for (org.bukkit.entity.Player p : plugin.getServer() .getOnlinePlayers()) { - reclaimAllTreasureItems(p); + reclaimPlayerItems(p.getName()); } + removeAllChests(); configs.clear(); activeChests.clear(); + refreshedChests.clear(); loadAll(); + plugin.getLogger().info( - "[寻宝] ★重载完成," + "[寻宝] ★重载完成,领取记录已清空," + configs.size() + " 个区域"); } + + // ★ 玩家寻宝临时数据 + private final Map + playerTreasureData = new HashMap<>(); + + public void setPlayerTreasureData( + String name, TreasureData data) { + playerTreasureData.put(name, data); + } + + public TreasureData getPlayerTreasureData( + String name) { + return playerTreasureData.get(name); + } + + public void clearPlayerTreasureData( + String name) { + playerTreasureData.remove(name); + } + + // 已刷新的宝箱坐标记录 + private final Set refreshedChests = + new HashSet<>(); + + /** 标记宝箱已刷新 */ + public void markRefreshed(Location loc) { + refreshedChests.add(locToString(loc)); + } + + /** 检查宝箱是否已刷新 */ + public boolean isRefreshed(Location loc) { + return refreshedChests.contains( + locToString(loc)); + } + + /** 清除刷新记录(重载/关闭时) */ + public void clearRefreshed() { + refreshedChests.clear(); + } + + private String locToString(Location loc) { + return loc.getWorld().getName() + + "," + loc.getBlockX() + + "," + loc.getBlockY() + + "," + loc.getBlockZ(); + } + /** 回收玩家身上所有寻宝物品(重载/关闭时用) */ public void reclaimAllTreasureItems( org.bukkit.entity.Player player) { @@ -201,40 +384,37 @@ public class TreasureManager { public void loadAll() { configs.clear(); + activeChests.clear(); File dir = new File( plugin.getDataFolder(), "寻宝"); dir.mkdirs(); + // ★ 第一步:自动迁移旧.txt文件 + migrateOldFiles(dir); + + // 第二步:读取.sdf1.txt文件 File[] files = dir.listFiles( (d, n) -> n.endsWith(CONFIG_SUFFIX)); if (files == null) return; - // ★ 黑名单 - java.util.List skip = - java.util.Arrays.asList( - "领取限制", - "配置说明", - "设置"); for (File f : files) { try { TreasureConfig tc = parseConfig(f); - if (tc != null) { + if (tc != null && tc != null) { configs.put(tc.name, tc); - loadSettings(tc); - configs.put(tc.name, tc); - plugin.getLogger().info( - "[寻宝] ★加载: " + tc.name + plugin.getLogger().info( + "[寻宝] ★加载: " + + tc.name + " 奖励数量=" + tc.rewards.size()); - // ★ 打印每条奖励 - for (int i = 0; + /* for (int i = 0; i < tc.rewards.size(); i++) { TreasureReward r = tc.rewards.get(i); plugin.getLogger().info( - "[寻宝] 奖励[" - + i + "]: " + "[寻宝] 奖励[" + i + + "]: " + r.getType() + " " + r.getMaterialName() @@ -244,21 +424,77 @@ public class TreasureManager { + r.getWeight() + " 名=" + r.getDisplayName()); - } + }*/ + loadSettings(tc); } } catch (Exception e) { plugin.getLogger().warning( - "[寻宝] 失败: " + f.getName() - + " - " + e.getMessage()); - e.printStackTrace(); + "[寻宝] 加载失败: " + + f.getName() + + " " + + e.getMessage()); } } + plugin.getLogger().info( - "[寻宝] 共" + configs.size() - + "个区域"); + "[寻宝] 共" + + configs.size() + "个区域"); } - /** 扫描玩家背包,回收不属于自己的物品 + 攻击次数用完的物品 */ + /** ★ 自动迁移旧.txt文件 → .sdf1.txt */ + private void migrateOldFiles(File dir) { + File[] oldFiles = dir.listFiles( + (d, name) -> name.endsWith(".txt") + && !name.endsWith(CONFIG_SUFFIX)); + + if (oldFiles == null) return; + + // ★ 黑名单:这些文件不处理 + java.util.Set blacklist = + new java.util.HashSet<>(); + blacklist.add("领取限制"); + blacklist.add("配置说明"); + blacklist.add("设置"); + + for (File f : oldFiles) { + String baseName = f.getName() + .replace(".txt", ""); + + // 黑名单跳过 + if (blacklist.contains(baseName)) continue; + + // 尝试读取第一行,确认是区域文件 + try { + BufferedReader br = + new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets.UTF_8)); + String first = br.readLine(); + br.close(); + + // 第一行包含"区域名"才迁移 + if (first != null + && first.contains("区域名")) { + String newName = + baseName + CONFIG_SUFFIX; + File newFile = new File( + f.getParent(), newName); + if (f.renameTo(newFile)) { + plugin.getLogger().info( + "[寻宝] ★迁移: " + + f.getName() + + " → " + + newName); + } + } + } catch (Exception ignored) { + } + } + } + + /** ★ 全场景扫描:过期/次数用完/非本人 → 全部回收 */ + /** ★ 全场景扫描回收 */ public void reclaimPlayerItems( String playerName) { org.bukkit.entity.Player player = @@ -266,10 +502,12 @@ public class TreasureManager { .getPlayerExact(playerName); if (player == null) return; + long now = System.currentTimeMillis(); int removed = 0; + for (int i = 0; - i < player.getInventory() - .getSize(); i++) { + i < player.getInventory().getSize(); + i++) { ItemStack item = player.getInventory() .getItem(i); @@ -280,32 +518,72 @@ public class TreasureManager { String[] mark = TreasureInventory .parseMark(item); - if (mark == null) continue; - - // mark[0]=原始玩家, mark[1]=区域, - // mark[2]=名称, mark[3]=上限, mark[4]=次数 - String owner = mark[0]; - int maxUse = - Integer.parseInt(mark[3]); - int useCount = - Integer.parseInt(mark[4]); - - boolean notMine = !owner - .equalsIgnoreCase(playerName); - boolean usedUp = - maxUse > 0 - && useCount >= maxUse; - - if (notMine || usedUp) { + if (mark == null) { + // ★ 无标记的自定义物品→直接清除 player.getInventory() .setItem(i, null); - removeClaim(owner, mark[2], - mark[1]); removed++; - if (notMine) { + continue; + } + + String owner = mark[0]; + String region = mark[1]; + String rName = mark[2]; + int maxUse = Integer.parseInt(mark[3]); + int useCount = + Integer.parseInt(mark[4]); + long expireTime = + Long.parseLong(mark[5]); + + boolean shouldRemove = false; + String reason = ""; + + // 检查1: 非本人 + if (!owner.equalsIgnoreCase( + playerName)) { + shouldRemove = true; + reason = "非本人持有"; + } + + // ★ 检查2: 无领取记录→视为过期 + if (!shouldRemove + && !isClaimed(playerName, + rName, region)) { + shouldRemove = true; + reason = "无领取记录(过期/重载)"; + } + + // 检查3: 已过期 + if (!shouldRemove + && expireTime > 0 + && now >= expireTime) { + shouldRemove = true; + reason = "已过期"; + } + + // 检查4: 攻击次数用完 + if (!shouldRemove + && maxUse > 0 + && useCount >= maxUse) { + shouldRemove = true; + reason = "次数用完"; + } + + if (shouldRemove) { + player.getInventory() + .setItem(i, null); + unclaim(playerName, rName, region); + removed++; + if (!owner.equalsIgnoreCase( + playerName)) { player.sendMessage( - "§c[寻宝] " + mark[2] + "§c[寻宝] " + rName + " 不属于你,已回收"); + } else { + player.sendMessage( + "§c[寻宝] " + rName + + " " + reason + + ",已回收"); } } } @@ -316,29 +594,63 @@ public class TreasureManager { + playerName + " " + removed + "件"); } +} + + /** 领取限时物品 */ + public void claim(String player, + String itemName, + String regionName) { + String key = player + "." + itemName + + "." + regionName; + claimDb.put(key, true); + saveClaims(); + } + + /** 取消领取限制 */ + public void unclaim(String player, + String itemName, + String regionName) { + String key = player + "." + itemName + + "." + regionName; + claimDb.remove(key); + saveClaims(); + } + + /** 检查是否已领取 */ + public boolean isClaimed(String player, + String itemName, + String regionName) { + String key = player + "." + + itemName + "." + regionName; + return claimDb.containsKey(key); } /** ★ 统一标点:中文→英文 */ private String normalizePunctuation(String s) { return s - .replace(":", ":") + .replace("\uff1a", ":") // :全角冒号 + .replace("\ufe58", ":") // ﹕小冒号 + .replace("\ua789", ":") // ꞉ + .replace(":", ":") // 冗余保险 + .replace("\uff0c", ",") // ,全角逗号 + .replace(",", ",") // 冗余保险 + .replace("\uff1b", ";") // ;全角分号 .replace(";", ";") - .replace(",", ",") - .replace("。", ".") + .replace("\uff08", "(") // ( .replace("(", "(") + .replace("\uff09", ")") // ) .replace(")", ")") - .replace("【", "[") - .replace("】", "]") - .replace("!", "!") - .replace("?", "?") + .replace("\uff5e", "~") // ~全角波浪 .replace("~", "~") + .replace("\u3001", ",") // 、顿号 .replace("、", ",") - .replace("=", "=") - .replace("+", "+") - .replace("-", "-") - .replace(" ", " ") - .replace("\uFEFF", ""); + .replace("\uff01", "!") // ! + .replace("!", "!") + .replace("\uff1f", "?") // ? + .replace("?", "?") + .replace("\u3000", " ") // 全角空格 + .replace("\ufeff", ""); // BOM } @@ -366,27 +678,20 @@ public class TreasureManager { "[寻宝] 文件共 " + lines.size() + " 行: " + f.getName()); - for (int idx = 0; - idx < lines.size(); idx++) { + for (int lineIdx = 0; + lineIdx < lines.size(); lineIdx++) { - String rawLine = lines.get(idx); - int lineNum = idx + 1; - - // ★ 打印原始行 - plugin.getLogger().info( - "[寻宝] 行" + lineNum - + ": [" + rawLine + "]"); + String rawLine = lines.get(lineIdx); + int lineNum = lineIdx + 1; String line = rawLine.trim(); if (line.isEmpty() || line.startsWith("#")) { - plugin.getLogger().info( - "[寻宝] → 跳过"); continue; } - // ★ 统一标点 - line = normalizePunctuation(line); + // ★ 只转全角冒号为半角 + line = line.replace('\uff1a', ':'); int ci = line.indexOf(':'); if (ci < 0) { @@ -398,9 +703,9 @@ public class TreasureManager { String k = line.substring(0, ci).trim(); String v = line.substring(ci + 1).trim(); - plugin.getLogger().info( + /* plugin.getLogger().info( "[寻宝] key=[" + k - + "] val=[" + v + "]"); + + "] val=[" + v + "]");*/ switch (k) { case "区域名": @@ -463,11 +768,6 @@ public class TreasureManager { parseReward(v); if (rr != null) { tc.rewards.add(rr); - int idx1 = - tc.rewards.size() - 1; - // ★ 记录中文名 - tc.rewardNames.put(idx, - rr.getDisplayName()); } } catch (Exception e) { plugin.getLogger().warning( @@ -475,7 +775,6 @@ public class TreasureManager { + e.getMessage()); } break; - default: plugin.getLogger().info( "[寻宝] 未知key: [" @@ -487,10 +786,12 @@ public class TreasureManager { plugin.getLogger().info( "[寻宝] 解析完成: " + tc.name - + " 奖励=" + tc.rewards.size()); + + " 奖励=" + + tc.rewards.size()); return tc; } + private int parseTime(String s) { s = s.trim().toLowerCase(); if (s.matches("\\d+")) @@ -619,6 +920,42 @@ public class TreasureManager { claimedSet.clear(); } + /** + * 清空指定玩家的所有领取记录(限时物品+寻宝物品) + * @param playerName 玩家名 + * @return 清除的记录数量 + */ + public int clearPlayerAllClaims(String playerName) { + int count = 0; + + // 1. 清除限时物品领取记录 (claimDb) - key格式: 玩家名.物品名.区域名 + Iterator> claimDbIterator = claimDb.entrySet().iterator(); + while (claimDbIterator.hasNext()) { + Map.Entry entry = claimDbIterator.next(); + if (entry.getKey().startsWith(playerName + ".")) { + claimDbIterator.remove(); + count++; + } + } + + // 2. 清除寻宝物品领取记录 (claimedSet) - key格式: 玩家名|物品名|区域名 + Iterator claimedSetIterator = claimedSet.iterator(); + while (claimedSetIterator.hasNext()) { + String key = claimedSetIterator.next(); + if (key.startsWith(playerName + "|")) { + claimedSetIterator.remove(); + count++; + } + } + + // 3. 保存更改到文件 + if (count > 0) { + saveClaims(); + rewriteClaimsFile(); + } + + return count; + } // ===== NBT 标记宝箱 ===== @@ -742,7 +1079,217 @@ public class TreasureManager { plugin.getLogger().info( "[寻宝] 中文解析: " + line); - // 1. 提取物品名 + // ★ 优先给玩家格式 + if (line.contains("给")) { + return parseGivePlayerItem(line); + } + // ★ 模糊匹配 + String matName = fuzzyMatchItem(line); + if (matName == null) { + plugin.getLogger().warning( + "[寻宝] ★无物品: " + line); + return null; + } + + String cnName = ""; + for (java.util.Map.Entry e + : ITEM_CN.entrySet()) { + if (e.getValue().equals(matName)) { + cnName = e.getKey(); + break; + } + } + + + // 数量 + int amount = 1; + java.util.regex.Matcher am = + java.util.regex.Pattern + .compile( + "([一二三四五六七八九十\\d]+)" + + "([个根把件套只])") + .matcher(line); + if (am.find()) { + amount = parseChineseNumber( + am.group(1)); + } + if (amount <= 1) { + java.util.regex.Matcher am2 = + java.util.regex.Pattern + .compile( + "(\\d+)\\s*[个根把件套只]") + .matcher(line); + if (am2.find()) { + amount = Integer.parseInt( + am2.group(1)); + } + } + + // 附魔 + String[] enchDefs = { + "水下速掘", "aqua_affinity", + "节肢杀手", "bane_of_arthropods", + "爆炸保护", "blast_protection", + "破甲", "breach", + "引雷", "channeling", + "绑定诅咒", "binding_curse", + "消失诅咒", "vanishing_curse", + "致密", "density", + "深海探索者", "depth_strider", + "效率", "efficiency", + "掉落缓冲", "feather_falling", + "火焰附加", "fire_aspect", + "火焰保护", "fire_protection", + "火矢", "flame", + "时运", "fortune", + "冰霜行者", "frost_walker", + "穿刺", "impaling", + "无限", "infinity", + "击退", "knockback", + "抢夺", "looting", + "忠诚", "loyalty", + "经验修补", "mending", + "多重射击", "multishot", + "穿透", "piercing", + "力量", "power", + "保护", "protection", + "冲击", "punch", + "快速装填", "quick_charge", + "水下呼吸", "respiration", + "激流", "riptide", + "锋利", "sharpness", + "精准采集", "silk_touch", + "亡灵杀手", "smite", + "灵魂疾行", "soul_speed", + "横扫之刃", "sweeping_edge", + "迅捷潜行", "swift_sneak", + "荆棘", "thorns", + "耐久", "unbreaking", + "风暴", "wind_burst", + }; + + List enchIds = new ArrayList<>(); + List enchLvls = new ArrayList<>(); + + for (int i = 0; + i < enchDefs.length; i += 2) { + String cn = enchDefs[i]; + String id = enchDefs[i + 1]; + if (!line.contains(cn)) continue; + + int lvl = 0; + java.util.regex.Matcher m1 = + java.util.regex.Pattern + .compile(cn + + "\\s*[,,]?\\s*等级?\\s*[::]?\\s*(\\d+)") + .matcher(line); + if (m1.find()) { + lvl = Integer.parseInt(m1.group(1)); + } + if (lvl <= 0) { + java.util.regex.Matcher m2 = + java.util.regex.Pattern + .compile(cn + "\\s*(\\d+)") + .matcher(line); + if (m2.find()) { + lvl = Integer.parseInt( + m2.group(1)); + } + } + if (lvl <= 0) { + java.util.regex.Matcher m3 = + java.util.regex.Pattern + .compile( + "(\\d+)\\s*级?" + cn) + .matcher(line); + if (m3.find()) { + lvl = Integer.parseInt( + m3.group(1)); + } + } + if (lvl <= 0) { + int pos = line.indexOf(cn); + if (pos >= 0) { + String after = line.substring( + pos + cn.length()); + java.util.regex.Matcher m4 = + java.util.regex.Pattern + .compile( + "\\D*(\\d+)") + .matcher(after); + if (m4.find()) { + int val = Integer.parseInt( + m4.group(1)); + if (val > 0 && val <= 255) { + lvl = val; + } + } + } + } + if (lvl <= 0) lvl = 1; + if (!enchIds.contains(id)) { + enchIds.add(id); + enchLvls.add(lvl); + } + } + + StringBuilder enchSB = new StringBuilder(); + for (int i = 0; i < enchIds.size(); i++) { + if (i > 0) enchSB.append(";"); + enchSB.append(enchIds.get(i)) + .append(",") + .append(enchLvls.get(i)); + } + String enchant = enchSB.length() > 0 + ? enchSB.toString() : null; + + // 时长 + int duration = 0; + java.util.regex.Matcher dm = + java.util.regex.Pattern + .compile( + "时长[::]?\\s*(\\d+)") + .matcher(line); + if (dm.find()) { + int val = Integer.parseInt(dm.group(1)); + duration = val < 100 ? val * 60 : val; + } + + // 攻击次数(可选) + int attackLimit = 0; + java.util.regex.Matcher atm = + java.util.regex.Pattern + .compile( + "攻击[^\\d]*[::]?\\s*(\\d+)") + .matcher(line); + if (atm.find()) { + attackLimit = Integer.parseInt( + atm.group(1)); + } + + // ★ 判断分支 + boolean hasEnch = enchant != null + && !enchant.isEmpty(); + + if (hasEnch || duration > 0 + || attackLimit > 0) { + return new TreasureReward( + TreasureReward.Type.ITEM, + matName, amount, 0, null, 10, + "§b" + cnName, + duration, enchant, null, + attackLimit); + } else { + return new TreasureReward( + TreasureReward.Type.ITEM, + matName, amount, 0, null, 10, + "§f" + cnName + " x" + amount); + } + } + + /** 处理 "给玩家XXX,附魔,等级,时长,攻击" 格式 */ + private TreasureReward parseGivePlayerItem( + String line) { String itemName = line .replace("给予玩家", "") .replace("给玩家", "") @@ -750,13 +1297,30 @@ public class TreasureManager { .replace("给", "") .trim(); + // ★ 模糊匹配 + String matName = fuzzyMatchItem(itemName); + if (matName == null) { + plugin.getLogger().warning( + "[寻宝] ★无法识别: " + line); + return null; + } + + String cnName = ""; + for (java.util.Map.Entry e + : ITEM_CN_TO_ID.entrySet()) { + if (e.getValue().equals(matName)) { + cnName = e.getKey(); + break; + } + } + + String cleanName = itemName .replaceAll( "[一二三四五六七八九十\\d]+[个根把件套只]", "") .trim(); - // 物品匹配 String[][] items = { {"旋风棒", "BREEZE_ROD"}, {"烈焰棒", "BLAZE_ROD"}, @@ -764,10 +1328,8 @@ public class TreasureManager { {"金苹果", "GOLDEN_APPLE"}, {"下界合金剑", "NETHERITE_SWORD"}, {"下界合金镐", "NETHERITE_PICKAXE"}, - {"下界合金锭", "NETHERITE_INGOT"}, {"钻石剑", "DIAMOND_SWORD"}, {"钻石镐", "DIAMOND_PICKAXE"}, - {"钻石斧", "DIAMOND_AXE"}, {"钻石甲", "DIAMOND_CHESTPLATE"}, {"钻石靴子", "DIAMOND_BOOTS"}, {"钻石护腿", "DIAMOND_LEGGINGS"}, @@ -779,17 +1341,13 @@ public class TreasureManager { {"铁锭", "IRON_INGOT"}, {"钻石", "DIAMOND"}, {"木棍", "STICK"}, - {"木剑", "WOODEN_SWORD"}, - {"弓", "BOW"}, - {"箭", "ARROW"}, - {"盾牌", "SHIELD"}, {"面包", "BREAD"}, {"不死图腾", "TOTEM_OF_UNDYING"}, {"煤炭", "COAL"}, - {"红石", "REDSTONE"} + {"红石", "REDSTONE"}, }; - String matName = null; + String matName2 = null; for (String[] pair : items) { if (cleanName.contains(pair[0])) { matName = pair[1]; @@ -803,9 +1361,7 @@ public class TreasureManager { } matName = matName.toUpperCase(); - - - // 2. 数量 + // 数量 int amount = 1; java.util.regex.Matcher am = java.util.regex.Pattern @@ -819,7 +1375,7 @@ public class TreasureManager { amount = parseChineseNumber(n); } - // 3. ★ 泛解析附魔(全行扫描) + // 附魔泛解析 String[] enchDefs = { "水下速掘", "aqua_affinity", "节肢杀手", "bane_of_arthropods", @@ -843,12 +1399,14 @@ public class TreasureManager { "抢夺", "looting", "忠诚", "loyalty", "海之眷顾", "luck_of_the_sea", - "突进", "lure", + "突进", "lunge", + "诱饵", "lure", "经验修补", "mending", "多重射击", "multishot", "穿透", "piercing", "力量", "power", - "弹射物保护", "projectile_protection", + "弹射物保护", + "projectile_protection", "保护", "protection", "冲击", "punch", "快速装填", "quick_charge", @@ -864,142 +1422,103 @@ public class TreasureManager { "耐久", "unbreaking", "风暴", "wind_burst", "风爆", "wind_burst", - }; List enchIds = new ArrayList<>(); - List enchLvls = new ArrayList<>(); + List enchLvls = + new ArrayList<>(); - // ★ 用"与"分割多个附魔 - String[] segments = line - .replace("与", "|") - .replace("、", "|") - .replace(",", "|") - .replace(";", "|") - .split("\\|"); + for (int i = 0; + i < enchDefs.length; i += 2) { + String cn = enchDefs[i]; + String id = enchDefs[i + 1]; + if (!line.contains(cn)) continue; - for (String seg : segments) { - String segTrim = seg.trim(); + int lvl = 0; - // ★ 先用全行匹配所有附魔 - for (int i = 0; i < enchDefs.length; - i += 2) { - String cn = enchDefs[i]; - String id = enchDefs[i + 1]; + // "附魔火焰附加,等级255" + java.util.regex.Matcher m1 = + java.util.regex.Pattern + .compile(cn + + "[,,]?\\s*等级?\\s*[::]?\\s*(\\d+)") + .matcher(line); + if (m1.find()) { + lvl = Integer.parseInt( + m1.group(1)); + } - if (!line.contains(cn)) continue; + // "击退255" + if (lvl <= 0) { + java.util.regex.Matcher m2 = + java.util.regex.Pattern + .compile(cn + + "\\s*(\\d+)") + .matcher(line); + if (m2.find()) { + lvl = Integer.parseInt( + m2.group(1)); + } + } - int lvl = 0; - - // 方式1: "附魔火焰附加,等级255" - // → "等级255"在附魔名之后的文本中 - java.util.regex.Matcher m1 = + // "255级锋利" + if (lvl <= 0) { + java.util.regex.Matcher m3 = java.util.regex.Pattern .compile( - cn + "[,,]?\\s*等级?\\s*[::]?\\s*(\\d+)") + "(\\d+)\\s*级?" + cn) .matcher(line); - if (m1.find()) { + if (m3.find()) { lvl = Integer.parseInt( - m1.group(1)); + m3.group(1)); } + } - // 方式2: "附魔击退255" 紧跟数字 - if (lvl <= 0) { - java.util.regex.Matcher m2 = + // 附魔名后最近的数字 + if (lvl <= 0) { + int pos = line.indexOf(cn); + if (pos >= 0) { + String after = line + .substring(pos + + cn.length()); + java.util.regex.Matcher m4 = java.util.regex.Pattern .compile( - cn + "\\s*(\\d+)") - .matcher(line); - if (m2.find()) { - lvl = Integer.parseInt( - m2.group(1)); - } - } - - // 方式3: "255级火焰附加" - if (lvl <= 0) { - java.util.regex.Matcher m3 = - java.util.regex.Pattern - .compile( - "(\\d+)\\s*级?" - + cn) - .matcher(line); - if (m3.find()) { - lvl = Integer.parseInt( - m3.group(1)); - } - } - - // 方式4: "锋利255与风暴3" 分割后 - // 在整个line中找该附魔名后最近的数字 - if (lvl <= 0) { - int pos = line.indexOf(cn); - if (pos >= 0) { - String after = - line.substring( - pos + cn.length()); - java.util.regex.Matcher m4 = - java.util.regex.Pattern - .compile( - "\\D*(\\d+)") - .matcher(after); - if (m4.find()) { - int val = Integer.parseInt( - m4.group(1)); - if (val > 0 && val <= 255) { - lvl = val; - } + "\\D*(\\d+)") + .matcher(after); + if (m4.find()) { + int val = + Integer.parseInt( + m4.group(1)); + if (val > 0 && val <= 255) { + lvl = val; } } } + } - if (lvl <= 0) lvl = 1; + if (lvl <= 0) lvl = 1; + + // 去重 + if (!enchIds.contains(id)) { enchIds.add(id); enchLvls.add(lvl); } } - - // ★ 额外安全网:直接全行扫数字 - // 如果上面没匹配到但全行有附魔名+数字 - if (enchIds.isEmpty()) { - for (int i = 0; i < enchDefs.length; - i += 2) { - String cn = enchDefs[i]; - String id = enchDefs[i + 1]; - if (!line.contains(cn)) continue; - - java.util.regex.Matcher m = - java.util.regex.Pattern - .compile( - "(\\d+)") - .matcher(line); - int lvl = 0; - while (m.find()) { - int val = Integer.parseInt( - m.group(1)); - if (val > 0 && val <= 255) { - lvl = val; - break; - } - } - if (lvl <= 0) lvl = 1; - enchIds.add(id); - enchLvls.add(lvl); - } - } - - StringBuilder enchSB = new StringBuilder(); - for (int i = 0; i < enchIds.size(); i++) { + StringBuilder enchSB = + new StringBuilder(); + for (int i = 0; + i < enchIds.size(); i++) { if (i > 0) enchSB.append(";"); enchSB.append(enchIds.get(i)) .append(",") .append(enchLvls.get(i)); } - String enchant = enchSB.length() > 0 - ? enchSB.toString() : null; + String enchant = + enchSB.length() > 0 + ? enchSB.toString() : null; - // 4. 时长 + // 时长 int duration = 0; java.util.regex.Matcher dm = java.util.regex.Pattern @@ -1012,7 +1531,7 @@ public class TreasureManager { dm.group(1).trim()); } - // 5. 攻击次数 + // 攻击次数 int attackLimit = 0; java.util.regex.Matcher atm = java.util.regex.Pattern @@ -1024,13 +1543,13 @@ public class TreasureManager { atm.group(1)); } - plugin.getLogger().info( + /* plugin.getLogger().info( "[寻宝] ★结果: " + matName + " x" + amount + " 附魔=" + enchant + " 时长=" + duration + " 攻击=" + attackLimit); - +*/ return new TreasureReward( TreasureReward.Type.ITEM, matName, amount, 0, null, 10, @@ -1038,16 +1557,32 @@ public class TreasureManager { duration, enchant, null, attackLimit); } + /** 获取容器拦截次数 */ + public int getContainerBlockCount( + String playerName) { + return containerBlockCount + .getOrDefault(playerName, 0); + } // ★ 2. isClaimed - public boolean isClaimed(String player, - String itemName, - String regionName) { - return claimedSet.contains( - player + "|" + itemName - + "|" + regionName); + private void saveClaims() { + if (claimFile == null) claimFile = new File( + plugin.getDataFolder(), "claims.yml"); + try { + FileWriter fw = new FileWriter( + claimFile); + for (String key : claimDb.keySet()) { + fw.write(key + "\n"); + } + fw.close(); + } catch (IOException e) { + plugin.getLogger().warning( + "[寻宝] 保存claims失败: " + + e.getMessage()); + } } + // ★ 3. addClaim public void addClaim(String player, String itemName, @@ -1096,21 +1631,196 @@ public class TreasureManager { } private int parseChineseNumber(String s) { - s = s.trim(); + if (s == null || s.trim().isEmpty()) return 1; + s = s.trim().toLowerCase(); + + // ★ 直接数字 + try { return Integer.parseInt(s); } + catch (Exception ignore) {} + + // ★ 英文数字 switch (s) { - case "一": case "1": return 1; - case "二": case "两": case "2": return 2; - case "三": case "3": return 3; - case "四": case "4": return 4; - case "五": case "5": return 5; - case "六": case "6": return 6; - case "七": case "7": return 7; - case "八": case "8": return 8; - case "九": case "9": return 9; - case "十": case "10": return 10; - default: - try { return Integer.parseInt(s); } - catch (Exception e) { return 1; } + case "one": return 1; + case "two": case "both": return 2; + case "three": return 3; + case "four": return 4; + case "five": return 5; + case "six": return 6; + case "seven": return 7; + case "eight": return 8; + case "nine": return 9; + case "ten": return 10; + case "eleven": return 11; + case "twelve": return 12; + case "thirteen": return 13; + case "fourteen": return 14; + case "fifteen": return 15; + case "sixteen": return 16; + case "seventeen": return 17; + case "eighteen": return 18; + case "nineteen": return 19; + case "twenty": return 20; + case "thirty": return 30; + case "forty": return 40; + case "fifty": return 50; + case "sixty": return 60; + case "seventy": return 70; + case "eighty": return 80; + case "ninety": return 90; + case "hundred": return 100; + } + + // ★ 罗马数字 + int roman = parseRoman(s); + if (roman > 0) return roman; + + // ★ 中文数字 + int cn = parseFullChinese(s); + if (cn > 0) return cn; + + // ★ 兜底:提取字符串中第一个数字 + java.util.regex.Matcher m = + java.util.regex.Pattern + .compile("(\\d+)") + .matcher(s); + if (m.find()) { + try { + return Integer.parseInt(m.group(1)); + } catch (Exception ignore) {} + } + + return 1; + } + + /** ★ 罗马数字解析 */ + private int parseRoman(String s) { + s = s.trim().toUpperCase(); + if (s.isEmpty()) return 0; + // 简单验证:只含IVXLCDM + if (!s.matches("[IVXLCDM]+")) return 0; + + int result = 0; + int prev = 0; + for (int i = s.length() - 1; i >= 0; i--) { + int val; + switch (s.charAt(i)) { + case 'I': val = 1; break; + case 'V': val = 5; break; + case 'X': val = 10; break; + case 'L': val = 50; break; + case 'C': val = 100; break; + case 'D': val = 500; break; + case 'M': val = 1000; break; + default: return 0; + } + if (val < prev) { + result -= val; + } else { + result += val; + } + prev = val; + } + return result > 0 && result <= 9999 + ? result : 0; + } + + /** ★ 完整中文数字解析 */ + private int parseFullChinese(String s) { + s = s.trim(); + + // ★ 简单中文数字(预定义) + String[][] cnNums = { + {"零", "0"}, {"〇", "0"}, + {"一", "1"}, {"壹", "1"}, + {"二", "2"}, {"两", "2"}, {"贰", "2"}, + {"三", "3"}, {"叁", "3"}, + {"四", "4"}, {"肆", "4"}, + {"五", "5"}, {"伍", "5"}, + {"六", "6"}, {"陆", "6"}, + {"七", "7"}, {"柒", "7"}, + {"八", "8"}, {"捌", "8"}, + {"九", "9"}, {"玖", "9"}, + {"十", "10"}, {"拾", "10"}, + {"十一", "11"}, {"十二", "12"}, + {"十三", "13"}, {"十四", "14"}, + {"十五", "15"}, {"十六", "16"}, + {"十七", "17"}, {"十八", "18"}, + {"十九", "19"}, + {"二十", "20"}, {"廿", "20"}, + {"二十一", "21"}, {"二十二", "22"}, + {"二十三", "23"}, {"二十四", "24"}, + {"二十五", "25"}, {"二十六", "26"}, + {"二十七", "27"}, {"二十八", "28"}, + {"二十九", "29"}, + {"三十", "30"}, {"卅", "30"}, + {"四十", "40"}, {"五十", "50"}, + {"六十", "60"}, {"七十", "70"}, + {"八十", "80"}, {"九十", "90"}, + {"一百", "100"}, {"一百", "100"}, + {"一千", "1000"}, {"一万", "10000"}, + {"一亿", "100000000"}, + }; + for (String[] pair : cnNums) { + if (s.equals(pair[0])) { + return Integer.parseInt(pair[1]); + } + } + + // ★ 复杂中文数字(二十、二百五、三千...) + int result = 0; + int current = 0; + + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + int digit = cnCharToDigit(c); + if (digit >= 0) { + current = current * 10 + digit; + } else if (c == '十' || c == '拾') { + if (current == 0) current = 1; + result += current * 10; + current = 0; + } else if (c == '百' || c == '佰') { + if (current == 0) current = 1; + result += current * 100; + current = 0; + } else if (c == '千' || c == '仟') { + if (current == 0) current = 1; + result += current * 1000; + current = 0; + } else if (c == '万' || c == '萬') { + if (current == 0) current = 1; + result = (result + current) * 10000; + current = 0; + } else if (c == '亿' || c == '億') { + if (current == 0) current = 1; + result = (result + current) + * 100000000; + current = 0; + } + } + result += current; + + return result > 0 ? result : 0; + } + + + /** 中文单字→数字 */ + private int cnCharToDigit(char c) { + switch (c) { + case '零': case '〇': return 0; + case '一': case '壹': return 1; + case '二': case '两': case '贰': + return 2; + case '三': case '叁': return 3; + case '四': case '肆': return 4; + case '五': case '伍': return 5; + case '六': case '陆': return 6; + case '七': case '柒': return 7; + case '八': case '捌': return 8; + case '九': case '玖': return 9; + case '廿': return 2; + case '卅': return 3; + default: return -1; } } @@ -1148,40 +1858,89 @@ public class TreasureManager { e.getWhoClicked(); // ★ 检查光标上的物品 + // ★ 检查光标物品(容器放入) if (e.getCursor() != null && TreasureInventory - .isCustomItem( - e.getCursor())) { - String[] mark = - TreasureInventory.parseMark( - e.getCursor()); - if (mark != null - && !mark[0] - .equalsIgnoreCase( - p.getName())) { - // 不是本人的→立即回收 + .isCustomItem(e.getCursor())) { + org.bukkit.entity.Player p0 = + (org.bukkit.entity.Player) + e.getWhoClicked(); + InventoryType topType = + e.getView().getTopInventory() + .getType(); + if (topType != InventoryType.PLAYER + && topType + != InventoryType.CRAFTING + && topType + != InventoryType.CREATIVE) { e.setCancelled(true); - e.getCursor().setAmount(0); - p.sendMessage( - "§c[寻宝] 该物品不属于你"); - return; - } - // ★ 光标有自定义物品+目标是容器→拦截 - Inventory top = e.getView() - .getTopInventory(); - InventoryType type = top.getType(); - if (type != InventoryType.PLAYER - && type != InventoryType.CRAFTING - && type != InventoryType.CREATIVE) { - e.setCancelled(true); - p.sendMessage( - "§c[寻宝] 该物品不可放入容器"); + // ★ 事不过三 + boolean confiscate = + plugin.getTreasureManager() + .onContainerBlock( + p.getName()); + if (confiscate) { + // 第3次:没收光标物品 + e.getCursor().setAmount(0); + p.sendMessage( + "§c[寻宝] 多次违规," + + "物品已被没收!"); + plugin.getLogger().info( + "[寻宝] ★没收: " + + p.getName()); + } return; } } - // ★ 检查被点击槽位的物品 + // ★ 检查槽位物品(归属+容器) ItemStack clicked = e.getCurrentItem(); + if (clicked != null + && TreasureInventory + .isCustomItem(clicked)) { + org.bukkit.entity.Player p2 = + (org.bukkit.entity.Player) + e.getWhoClicked(); + // 非本人→直接没收 + String[] mark = + TreasureInventory + .parseMark(clicked); + if (mark != null + && !mark[0].equalsIgnoreCase( + p.getName())) { + e.setCancelled(true); + e.getCurrentItem().setAmount(0); + p.sendMessage( + "§c[寻宝] 该物品不属于你,已没收"); + return; + } + // 目标是容器→事不过三 + InventoryType topType = + e.getView().getTopInventory() + .getType(); + if (topType != InventoryType.PLAYER + && topType + != InventoryType.CRAFTING + && topType + != InventoryType.CREATIVE) { + e.setCancelled(true); + boolean confiscate = + plugin.getTreasureManager() + .onContainerBlock( + p.getName()); + if (confiscate) { + e.getCurrentItem() + .setAmount(0); + p.sendMessage( + "§c[寻宝] 多次违规," + + "物品已被没收!"); + } + } + } + + + // ★ 检查被点击槽位的物品 + ItemStack clicked1 = e.getCurrentItem(); if (clicked != null && TreasureInventory .isCustomItem(clicked)) { @@ -1314,89 +2073,143 @@ public class TreasureManager { * 物品,数量,权重,附魔,等级,显示名,时长秒 */ private TreasureReward parseReward(String line) { - plugin.getLogger().info( - "[寻宝] ★parseReward收到: [" - + line + "]"); - line = normalizePunctuation(line); - plugin.getLogger().info( - "[寻宝] ★parseReward归一化: [" - + line + "]"); - line = normalizePunctuation(line); + // ★ 只转冒号,不动逗号 + line = line.replace('\uff1a', ':'); + + /* plugin.getLogger().info( + "[寻宝] 解析奖励: " + line);*/ + if (line.charAt(0) == '\uFEFF') line = line.substring(1); - line = line.replace(",", ",") - .replace(":", ":"); - - if (line.startsWith("贴标:")) { - return parseCustomItem( - line.substring("贴标:".length())); - } - plugin.getLogger().info( - "[寻宝] 解析奖励: " + line); + // 贴标 if (line.startsWith("贴标:")) { return parseCustomItem( line.substring("贴标:".length())); } - if (line.contains("给予") - || line.contains("附魔") - || line.contains("时长")) { - plugin.getLogger().info( - "[寻宝] → 走中文分支"); - return parseChineseReward(line); - } - - String[] p = line.split(","); - if (p.length < 2) return null; - String type = p[0].trim().toUpperCase(); - + String upper = line.toUpperCase(); // ★ 债券 - if (type.equals("债券")) { - String amtStr = p[1].trim(); - int w = p.length > 2 - ? Integer.parseInt(p[2].trim()) - : 10; - int bMin, bMax; - if (amtStr.contains("~")) { - String[] r = amtStr.split("~"); - bMin = Integer.parseInt(r[0].trim()); - bMax = Integer.parseInt(r[1].trim()); - } else if (amtStr.contains("-")) { - String[] r = amtStr.split("-"); - bMin = Integer.parseInt(r[0].trim()); - bMax = Integer.parseInt(r[1].trim()); - } else { - bMin = Integer.parseInt(amtStr); - bMax = bMin; + if (upper.contains("债券")) { + String after = line + .replaceAll("(?i)债券", "") + .trim(); + + int bMin = 1; + int bMax = 1; + int w = 25; + + if (!after.isEmpty()) { + String[] parts = after.split(","); + String rangePart = parts[0].trim(); + + if (parts.length >= 2) { + try { + w = parseChineseNumber( + parts[1].trim()); + } catch (Exception ignore) { + } + } + + // ★ 范围:~ 或 - 分隔 + if (rangePart.contains("~")) { + String[] r = + rangePart.split("~"); + bMin = parseChineseNumber( + r[0].trim()); + bMax = parseChineseNumber( + r[1].trim()); + } else if (rangePart.contains("-")) { + String[] r = + rangePart.split("-"); + bMin = parseChineseNumber( + r[0].trim()); + bMax = parseChineseNumber( + r[1].trim()); + } else { + bMin = parseChineseNumber( + rangePart); + bMax = bMin; + } } - TreasureReward tr = new TreasureReward( - TreasureReward.Type.BOND, - null, 1, bMin, null, w, - "§6" + bMin + "~" + bMax + "债券"); - tr.setBondRange(bMin, bMax); + + final int fbMin = bMin; + final int fbMax = bMax; + final int fw = w; + + TreasureReward tr = + new TreasureReward( + TreasureReward.Type.BOND, + null, 1, fbMin, null, fw, + "§6" + fbMin + + "~" + fbMax + + "债券"); + tr.setBondRange(fbMin, fbMax); + + plugin.getLogger().info( + "[寻宝] → 债券: " + + fbMin + "~" + fbMax + + " 权重=" + fw); return tr; } - // ★ 命令 - if (type.equals("命令")) { - String cmd = p[1].trim(); - int w = p.length > 2 - ? Integer.parseInt(p[2].trim()) - : 10; + + // 命令 + if (upper.contains("命令")) { + int ci = line.indexOf(','); + if (ci < 0) ci = line.indexOf(':'); + String cmd = ci > 0 + ? line.substring(ci + 1).trim() + : ""; + int w = 10; + if (cmd.contains(",")) { + String[] parts = cmd.split(","); + cmd = parts[0].trim(); + w = Integer.parseInt( + parts[1].trim()); + } return new TreasureReward( TreasureReward.Type.COMMAND, null, 1, 0, cmd, w, "§a命令奖励"); } - // ★ 物品 + // 中文分支(给玩家/附魔/时长/攻击) + if (line.contains("给") + || line.contains("附魔") + || line.contains("时长") + || line.contains("攻击")) { + return parseGivePlayerItem(line); + } + + // 中文物品简写(面包,64,10) + String[] commaParts = line.split(","); + if (commaParts.length >= 2) { + String firstPart = + commaParts[0].trim(); + boolean hasCN = false; + for (char c : firstPart.toCharArray()) { + if (c > 0x4E00) { + hasCN = true; + break; + } + } + if (hasCN) { + return parseChineseReward(line); + } + } + + // 英文格式(DIAMOND,1,10) + line = line.replace(",", ","); + String[] p = line.split(","); + if (p.length < 2) return null; + String type = p[0].trim().toUpperCase(); + int amt = Integer.parseInt(p[1].trim()); int w = p.length > 2 ? Integer.parseInt(p[2].trim()) : 10; -//打好包了,直接上传测试即可 - // ★ 中文名映射 + String[][] cnNames = { {"DIAMOND", "钻石"}, {"GOLD_INGOT", "金锭"}, @@ -1412,22 +2225,18 @@ public class TreasureManager { {"DIAMOND_LEGGINGS", "钻石护腿"}, {"DIAMOND_HELMET", "钻石头盔"}, {"NETHERITE_SWORD", "下界合金剑"}, - {"NETHERITE_PICKAXE", "下界合金镐"}, - {"NETHERITE_INGOT", "下界合金锭"}, + {"NETHERITE_PICKAXE", + "下界合金镐"}, + {"NETHERITE_INGOT", + "下界合金锭"}, {"BLAZE_ROD", "烈焰棒"}, {"BREEZE_ROD", "旋风棒"}, {"ENDER_PEARL", "末影珍珠"}, - {"GOLDEN_APPLE", "金苹果"}, - {"ENCHANTED_GOLDEN_APPLE", - "附魔金苹果"}, - {"TOTEM_OF_UNDYING", "不死图腾"}, + {"BREAD", "面包"}, + {"STICK", "木棍"}, {"BOW", "弓"}, {"ARROW", "箭"}, {"SHIELD", "盾牌"}, - {"BREAD", "面包"}, - {"STICK", "木棍"}, - {"IRON_SWORD", "铁剑"}, - {"WOODEN_SWORD", "木剑"}, }; String cnName = type; @@ -1438,14 +2247,62 @@ public class TreasureManager { } } - String displayName = - "§f" + cnName + " x" + amt; - return new TreasureReward( TreasureReward.Type.ITEM, - type, amt, 0, null, w, displayName); + type, amt, 0, null, w, + "§f" + cnName + " x" + amt); } + public void saveConfig(TreasureConfig tc) { + File dir = new File( + plugin.getDataFolder(), "寻宝"); + dir.mkdirs(); + File f = new File(dir, + tc.name + ".sdf1.txt"); + try { + OutputStreamWriter w = + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8); + w.write("区域名: " + tc.name + "\n"); + w.write("世界: " + tc.world + "\n"); + w.write("坐标: " + tc.minX + "," + + tc.minZ + "," + + tc.maxX + "," + + tc.maxZ + "\n"); + w.write("高度: " + tc.minY + "," + + tc.maxY + "\n"); + w.write("最多宝箱: " + tc.maxChests + "\n"); + w.write("检查间隔: " + + tc.spawnInterval + "秒\n"); + w.write("生成概率: " + tc.spawnChance + "\n"); + w.write("随机高度: " + + (tc.randomY ? "是" : "否") + "\n"); + for (TreasureReward r : tc.rewards) { + w.write("奖励: " + rewardToString(r) + "\n"); + } + w.close(); + } catch (IOException e) { + plugin.getLogger().warning( + "[寻宝] 保存失败: " + e.getMessage()); + } + } + + private String rewardToString(TreasureReward r) { + if (r.getType() == TreasureReward.Type.BOND) { + return "债券," + r.getBondMin() + + "~" + r.getBondMax() + + "," + r.getWeight(); + } + if (r.getType() == TreasureReward.Type.COMMAND) { + return "命令," + r.getCommand() + + "," + r.getWeight(); + } + String name = r.getMaterialName(); + int amt = r.getAmount(); + int w = r.getWeight(); + return name + "," + amt + "," + w; + } public void loadSettings(TreasureConfig tc) { @@ -1496,8 +2353,20 @@ public class TreasureManager { rr[1].trim()); } break; + case "刷新费用": + tc.refreshBondCost = + Integer.parseInt( + v.trim()); + break; + case "刷新最低奖励": + tc.refreshMinRewards = + Integer.parseInt( + v.trim()); + break; } } + + } catch (Exception e) { plugin.getLogger().warning( "[寻宝] 设置加载失败: " @@ -1547,46 +2416,53 @@ public class TreasureManager { public boolean saveRegion(String name, String world, - int x1, int z1, - int x2, int z2, + int minX, int minZ, + int maxX, int maxZ, int minY, int maxY) { - int a = Math.min(x1, x2); - int b = Math.max(x1, x2); - x1 = a; x2 = b; - a = Math.min(z1, z2); - b = Math.max(z1, z2); - z1 = a; z2 = b; File dir = new File( plugin.getDataFolder(), "寻宝"); dir.mkdirs(); - File f = new File(dir, name + ".txt"); - List lines = new ArrayList<>(); - lines.add("区域名: " + name); - lines.add("世界: " + world); - lines.add("坐标: " + x1 + "," + z1 - + "," + x2 + "," + z2); - lines.add("高度: " + minY + "," + maxY); - lines.add("最多宝箱: 3"); - lines.add("检查间隔: 5分钟"); - lines.add("生成概率: 100"); - lines.add("奖励: DIAMOND,1,10"); - lines.add("奖励: GOLD_INGOT,3,20"); - lines.add("奖励: 债券,50,25"); + // ★ 确认后缀是 .sdf1.txt + File f = new File(dir, + name + ".sdf1.txt"); try { OutputStreamWriter w = new OutputStreamWriter( new FileOutputStream(f), StandardCharsets.UTF_8); - for (String l : lines) - w.write(l + "\n"); + w.write("区域名: " + name + "\n"); + w.write("世界: " + world + "\n"); + w.write("坐标: " + minX + "," + + minZ + "," + + maxX + "," + maxZ + "\n"); + w.write("高度: " + minY + "," + + maxY + "\n"); + w.write("最多宝箱: 2\n"); + w.write("检查间隔: 5分钟\n"); + w.write("生成概率: 100\n"); + w.write("奖励: DIAMOND,1,10\n"); w.close(); } catch (IOException e) { + plugin.getLogger().warning( + "[寻宝] 保存失败: " + + e.getMessage()); return false; } - loadAll(); + + // ★ 加入内存map + TreasureConfig tc = new TreasureConfig(name); + tc.world = world; + tc.minX = minX; + tc.maxX = maxX; + tc.minZ = minZ; + tc.maxZ = maxZ; + tc.minY = minY; + tc.maxY = maxY; + configs.put(name, tc); + return true; } @@ -1728,7 +2604,7 @@ public class TreasureManager { activeChests.remove(oldest); } - // ★ 50次尝试找合法位置 + // ★ 50次尝试 for (int attempt = 0; attempt < 50; attempt++) { int x = ThreadLocalRandom.current() @@ -1738,30 +2614,22 @@ public class TreasureManager { int y = ThreadLocalRandom.current() .nextInt(tc.minY, tc.maxY + 1); - Location loc = - new Location(w, x, y, z); - Block block = loc.getBlock(); - Block above = loc.getBlock() - .getRelative( - org.bukkit.block.BlockFace.UP); + // ★ 智能搜索2格空气 + Location spot = findTwoAirSpot( + w, x, y, z, tc); + if (spot == null) continue; - // ★ 条件1: 当前位置不是箱子 - if (block.getType() == Material.CHEST) - continue; - - // ★ 条件2: 上方1格是空气(开箱空间) - if (above.getType() != Material.AIR) - continue; - - // ★ 放置箱子 + Block block = spot.getBlock(); block.setType(Material.CHEST); - tagChest(loc, tc.name); - activeChests.put(loc, tc.name); + tagChest(spot, tc.name); + activeChests.put(spot, tc.name); plugin.getLogger().info( "[寻宝] ★生成: " + tc.name - + " [" + x + "," - + y + "," + z + "]"); + + " [" + spot.getBlockX() + + "," + spot.getBlockY() + + "," + spot.getBlockZ() + + "]"); if (borderShowing && borderRegionName != null @@ -1773,10 +2641,117 @@ public class TreasureManager { } plugin.getLogger().warning( - "[寻宝] ★生成失败: " + tc.name); + "[寻宝] ★生成失败: " + tc.name + + " 50次未找到位置"); return false; } + /** ★ 从指定坐标开始,上下搜索2格空气 */ + private Location findTwoAirSpot( + World w, int x, int startY, int z, + TreasureConfig tc) { + // 先检查原位 + Location origin = + new Location(w, x, startY, z); + if (origin.getBlock().getType() + == Material.AIR + && origin.getBlock() + .getRelative( + org.bukkit.block.BlockFace.UP) + .getType() == Material.AIR) { + return origin; + } + + // ★ 从原位向下搜索 + for (int y = startY - 1; + y >= tc.minY; y--) { + Location loc = + new Location(w, x, y, z); + Block bottom = loc.getBlock(); + Block top = bottom.getRelative( + org.bukkit.block.BlockFace.UP); + if (bottom.getType() == Material.AIR + && top.getType() + == Material.AIR) { + return loc; + } + } + + // ★ 从原位向上搜索 + for (int y = startY + 1; + y <= tc.maxY; y++) { + Location loc = + new Location(w, x, y, z); + Block bottom = loc.getBlock(); + Block top = bottom.getRelative( + org.bukkit.block.BlockFace.UP); + if (bottom.getType() == Material.AIR + && top.getType() + == Material.AIR) { + return loc; + } + } + + return null; + } + /** ★ 公布宝箱坐标 */ + public boolean announceChest() { + return announceChest(null); + } + + /** ★ 公布指定区域最旧宝箱 */ + public boolean announceChest( + String regionName) { + if (activeChests.isEmpty()) return false; + + Location oldest = null; + String oldestRegion = null; + long oldestTime = Long.MAX_VALUE; + + for (java.util.Map.Entry e + : activeChests.entrySet()) { + Location loc = e.getKey(); + String region = e.getValue(); + if (loc.getWorld() == null) continue; + if (loc.getBlock().getType() + != Material.CHEST) continue; + + // ★ 指定了区域就只找该区域 + if (regionName != null + && !regionName + .equalsIgnoreCase(region)) { + continue; + } + + long time = getChestTime(loc); + if (time < oldestTime) { + oldestTime = time; + oldest = loc; + oldestRegion = region; + } + } + + if (oldest == null) return false; + + int cx = oldest.getBlockX(); + int cz = oldest.getBlockZ(); + + for (org.bukkit.entity.Player p + : plugin.getServer() + .getOnlinePlayers()) { + p.sendMessage("§6§l[寻宝] §e" + + (oldestRegion != null + ? oldestRegion : "") + + " §a宝藏出现在 X:" + + cx + " Z:" + cz); + p.sendMessage("§7快去寻找吧!"); + } + + plugin.getLogger().info( + "[寻宝] ★公布: " + oldestRegion + + " [" + cx + "," + cz + "]"); + return true; + } /** 清除带NBT标记但不在记录中的残留宝箱 */ public void cleanResidualChests() { diff --git a/src/main/java/Sdf1_game/TreasureSpawnTask.java b/src/main/java/Sdf1_game/TreasureSpawnTask.java index 83b1d23..58fb22b 100644 --- a/src/main/java/Sdf1_game/TreasureSpawnTask.java +++ b/src/main/java/Sdf1_game/TreasureSpawnTask.java @@ -11,6 +11,7 @@ public class TreasureSpawnTask extends BukkitRunnable { private final TreasureManager manager; private int offlineCount = 0; private boolean paused = false; + private boolean forceNextSpawn = false; public TreasureSpawnTask(Main plugin, TreasureManager manager) { @@ -41,8 +42,9 @@ public class TreasureSpawnTask extends BukkitRunnable { if (paused) { paused = false; + forceNextSpawn = true; plugin.getLogger().info( - "[寻宝] ★玩家上线"); + "[寻宝] ★玩家上线,下次生成100%"); } offlineCount = 0; @@ -65,6 +67,16 @@ public class TreasureSpawnTask extends BukkitRunnable { long need = tc.spawnInterval * 1000L; if (elapsed < need) continue; + // ★ 暂停恢复后首次生成:100%生成 + if (forceNextSpawn) { + forceNextSpawn = false; + manager.spawnChest(tc); + tc.lastSpawnTime = now; + broadcastSpawn(tc); + continue; + } + + // 按配置概率判定 int roll = ThreadLocalRandom.current() .nextInt(1, 101); if (roll > tc.spawnChance) { @@ -76,8 +88,6 @@ public class TreasureSpawnTask extends BukkitRunnable { tc.lastSpawnTime = now; // ★ 通知所有玩家 broadcastSpawn(tc); - plugin.getLogger().info( - "[寻宝] ★生成: " + tc.name); } } private void broadcastSpawn(TreasureConfig tc) { diff --git a/src/main/java/Sdf1_game/UpdateChecker.java b/src/main/java/Sdf1_game/UpdateChecker.java new file mode 100644 index 0000000..8da3c7a --- /dev/null +++ b/src/main/java/Sdf1_game/UpdateChecker.java @@ -0,0 +1,442 @@ +package Sdf1_game; + +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +public class UpdateChecker { + + // ===== SSL证书信任所有(解决PKIX错误) ===== + static { + try { + TrustManager[] trustAllCerts = new TrustManager[]{ + new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } + public void checkClientTrusted(X509Certificate[] certs, String authType) {} + public void checkServerTrusted(X509Certificate[] certs, String authType) {} + } + }; + SSLContext sc = SSLContext.getInstance("TLS"); + sc.init(null, trustAllCerts, new SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + } catch (Exception ignored) {} + } + + private final Main plugin; + private final String currentVersion; + + // ===== 仓库信息 ===== + private static final String GITHUB_OWNER = "ypsdf1"; + private static final String GITHUB_REPO = "Sdf1_games"; + private static final String GITEE_OWNER = "nihaoshidifu"; + private static final String GITEE_REPO = "sdf1_game"; + + // API 地址(使用releases/latest获取最新版本) + private static final String GITHUB_API = + "https://api.github.com/repos/" + GITHUB_OWNER + "/" + GITHUB_REPO + "/releases/latest"; + private static final String GITEE_API = + "https://gitee.com/api/v5/repos/" + GITEE_OWNER + "/" + GITEE_REPO + "/releases/latest"; + + // 仓库页面链接 + private static final String GITHUB_LINK = + "https://github.com/" + GITHUB_OWNER + "/" + GITHUB_REPO + "/releases"; + private static final String GITEE_LINK = + "https://gitee.com/" + GITEE_OWNER + "/" + GITEE_REPO + "/releases"; + + // 单次请求超时(毫秒) + private static final int TIMEOUT_MS = 5000; + + // 熔断阈值 + private static final int TRIP_THRESHOLD = 5; + + // 熔断后重试间隔(2分钟) + private static final long RETRY_DELAY_MS = 2 * 60 * 1000; + + // ===== 熔断状态(内存态,重启自动清除) ===== + private volatile boolean circuitTripped = false; + private volatile int failCount = 0; + private volatile boolean manualSuccess = false; + + // ===== 自动检测禁用(持久化到文件) ===== + private volatile boolean autoCheckDisabled = false; + private final File disabledFile; + + public UpdateChecker(Main plugin) { + this.plugin = plugin; + this.currentVersion = plugin.getDescription().getVersion(); + this.disabledFile = new File(plugin.getDataFolder(), "update_disabled.dat"); + loadDisabledState(); + } + + // ========== 持久化禁用状态 ========== + + private void loadDisabledState() { + if (disabledFile.exists()) { + try (BufferedReader br = new BufferedReader(new FileReader(disabledFile))) { + String line = br.readLine(); + if (line != null) { + autoCheckDisabled = "true".equals(line.trim()); + } + } catch (Exception ignored) { + } + } + } + + private void saveDisabledState() { + try (PrintWriter pw = new PrintWriter(new FileWriter(disabledFile))) { + pw.println(autoCheckDisabled ? "true" : "false"); + } catch (Exception ignored) { + } + } + + // ========== 公开方法 ========== + + /** + * onEnable 调用(异步静默) + * 熔断或禁用则直接跳过 + */ + public void checkOnEnable() { + if (autoCheckDisabled) return; + if (circuitTripped) { + // 熔断状态下,2分钟后重试 + scheduleRetry(); + return; + } + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> doCheck(null, true)); + } + + /** + * /treasure update 或 /quiz update 调用(异步) + * 忽略熔断和禁用状态,强制检测 + */ + public void checkUpdate(CommandSender sender) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> doCheck(sender, false)); + } + + /** + * 2分钟后重试 + */ + private void scheduleRetry() { + Bukkit.getScheduler().runTaskLater(plugin, () -> { + if (circuitTripped && !manualSuccess) { + plugin.getLogger().info("[Sdf1_game] 熔断重试中..."); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> doCheck(null, true)); + } + }, RETRY_DELAY_MS / 50); // ticks + } + + // ========== 核心检测逻辑 ========== + private void doCheck(CommandSender sender, boolean isAuto) { + + if (isAuto) { + if (autoCheckDisabled || circuitTripped) { + return; + } + plugin.getLogger().info("[Sdf1_game] 启动自动检查更新..."); + } + + // ★ 双通道并行执行,一路成功掐断另一路 + AtomicBoolean cancelled = new AtomicBoolean(false); + AtomicReference ghResult = new AtomicReference<>(); + AtomicReference geResult = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(2); + + // 通道1:GitHub + Thread ghThread = new Thread(() -> { + if (!cancelled.get()) { + ghResult.set(tryChannel(GITHUB_API, "GitHub", GITHUB_LINK)); + if (ghResult.get().isSuccess()) { + cancelled.set(true); // 成功则取消另一路 + } + } + latch.countDown(); + }); + ghThread.setDaemon(true); + + // 通道2:Gitee + Thread geThread = new Thread(() -> { + if (!cancelled.get()) { + geResult.set(tryChannel(GITEE_API, "Gitee", GITEE_LINK)); + if (geResult.get().isSuccess()) { + cancelled.set(true); // 成功则取消另一路 + } + } + latch.countDown(); + }); + geThread.setDaemon(true); + + ghThread.start(); + geThread.start(); + + try { + latch.await(); // 等待两路都完成 + } catch (InterruptedException ignored) { + } + + CheckResult gh = ghResult.get(); + CheckResult ge = geResult.get(); + + // 选择成功的结果 + CheckResult chosen = null; + + if (gh != null && gh.isSuccess()) { + chosen = gh; + } else if (ge != null && ge.isSuccess()) { + chosen = ge; + } else { + // 双通道均失败 + failCount++; + StringBuilder sb = new StringBuilder(); + if (gh != null && gh.error != null) { + sb.append("§7GitHub: §c").append(gh.error); + } + if (ge != null && ge.error != null) { + if (sb.length() > 0) sb.append("\n"); + sb.append("§7Gitee: §c").append(ge.error); + } + if (failCount >= TRIP_THRESHOLD) { + circuitTripped = true; + plugin.getLogger().warning( + "[Sdf1_game] 更新检测已熔断,连续失败" + failCount + "次"); + // 熔断后安排2分钟后重试 + scheduleRetry(); + } + if (sender != null) { + final String msg = sb.toString(); + final boolean tripped = circuitTripped; + final int fc = failCount; + Bukkit.getScheduler().runTask(plugin, () -> { + sender.sendMessage("§c§l[更新] 检测失败"); + sender.sendMessage(msg); + sender.sendMessage("§e失败次数: " + fc + "/" + TRIP_THRESHOLD); + if (tripped) { + sender.sendMessage("§c已熔断,2分钟后自动重试,或重启服务器重置"); + } + }); + } + return; + } + + // 至少一个通道成功 + failCount = 0; + + // 检查禁用信号 + if (isDisableSignal(chosen.version)) { + autoCheckDisabled = true; + saveDisabledState(); + plugin.getLogger().info( + "[Sdf1_game] 远程版本信号 \"" + chosen.version + "\",自动更新检测已禁用"); + if (sender != null) { + Bukkit.getScheduler().runTask(plugin, () -> + sender.sendMessage("§e[更新] 远程已禁用自动更新检测")); + } + return; + } + + // 手动调用:成功才解除熔断和禁用 + if (!isAuto) { + manualSuccess = true; // 标记手动成功,不再执行后续自动检查 + if (circuitTripped) { + circuitTripped = false; + } + if (autoCheckDisabled) { + autoCheckDisabled = false; + saveDisabledState(); + } + } + + final String src = chosen.source; + final String lnk = chosen.link; + final String rv = chosen.version; + + // ★ 自动检查也记录日志(含下载链接 + 颜色提醒) + if (isAuto) { + boolean hasUpdate = isNewer(rv, currentVersion); + String status = hasUpdate + ? "(有新版本!)" + : "(已是最新)"; + // 使用Bukkit.getConsoleSender().sendMessage()输出彩色日志 + Bukkit.getConsoleSender().sendMessage("§a[Sdf1_game] §e更新检测成功 via: " + src + + " | 本地: " + currentVersion + " | 远程: " + rv + " §7" + status); + if (hasUpdate) { + Bukkit.getConsoleSender().sendMessage("§a[Sdf1_game] §b下载: " + lnk); + } + } + + Bukkit.getScheduler().runTask(plugin, () -> { + if (sender == null) return; + + sender.sendMessage("§e[Sdf1_game] [更新] 检测成功 via: §a" + src); + sender.sendMessage("§e[Sdf1_game] [更新] 本地版本: §f" + currentVersion); + sender.sendMessage("§e[Sdf1_game] [更新] 远程版本: §f" + rv); + + if (isNewer(rv, currentVersion)) { + sender.sendMessage("§c[Sdf1_game] [更新] 发现新版本: " + rv); + sender.sendMessage("§a[Sdf1_game] 请前往仓库下载更新:"); + sender.sendMessage("§a§l" + lnk); + } else { + sender.sendMessage("§a[Sdf1_game] [更新] 已是最新版本"); + } + + // 显示另一通道的失败原因 + CheckResult other = (src.equals("GitHub")) ? ge : gh; + if (other != null && !other.isSuccess() && other.error != null) { + sender.sendMessage("§7[Sdf1_game] [更新] " + other.source + " 不可用: " + other.error); + } + }); + } + + // ========== 禁用信号判断 ========== + + private boolean isDisableSignal(String version) { + if (version == null) return false; + String v = version.trim().toLowerCase(); + return "999".equals(v) || "关闭".equals(v) || "false".equals(v) || "禁用".equals(v); + } + + // ========== HTTP 请求 ========== + + private static class CheckResult { + final String version; + final String source; + final String link; + final String error; + + CheckResult(String version, String source, String link, String error) { + this.version = version; + this.source = source; + this.link = link; + this.error = error; + } + + boolean isSuccess() { + return version != null; + } + } + + private CheckResult tryChannel(String apiUrl, String sourceName, String link) { + HttpURLConnection conn = null; + try { + URL url = new URL(apiUrl); + conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("User-Agent", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + + "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"); + conn.setRequestProperty("Accept", "*/*"); + conn.setRequestProperty("Accept-Language", "zh-CN,zh;q=0.9"); + conn.setConnectTimeout(TIMEOUT_MS); + conn.setReadTimeout(TIMEOUT_MS); + conn.setInstanceFollowRedirects(true); + + int code = conn.getResponseCode(); + + if (code == 404) { + return new CheckResult(null, sourceName, link, sourceName + ": 未找到 (HTTP 404)"); + } + if (code == 403) { + return new CheckResult(null, sourceName, link, sourceName + ": 被限流 (HTTP 403)"); + } + if (code != 200) { + return new CheckResult(null, sourceName, link, sourceName + ": HTTP " + code); + } + + BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + reader.close(); + + String tag = parseTagName(sb.toString()); + if (tag == null) { + return new CheckResult(null, sourceName, link, sourceName + ": 无法解析版本号"); + } + + return new CheckResult(tag, sourceName, link, null); + + } catch (java.net.SocketTimeoutException e) { + return new CheckResult(null, sourceName, link, sourceName + ": 连接超时"); + } catch (java.net.ConnectException e) { + return new CheckResult(null, sourceName, link, sourceName + ": 连接被拒绝"); + } catch (java.net.UnknownHostException e) { + return new CheckResult(null, sourceName, link, sourceName + ": DNS解析失败"); + } catch (Exception e) { + return new CheckResult(null, sourceName, link, sourceName + ": " + e.getMessage()); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + } + + private String parseTagName(String json) { + // tags API 返回数组: [{"name":"v1.1",...}] + // releases API 返回对象: {"tag_name":"v1.1"} + + int idx = json.indexOf("\"tag_name\""); + if (idx >= 0) { + int colon = json.indexOf(":", idx); + int q1 = json.indexOf("\"", colon + 1); + int q2 = json.indexOf("\"", q1 + 1); + if (q2 > q1) { + return cleanTag(json.substring(q1 + 1, q2)); + } + } + + idx = json.indexOf("\"name\""); + if (idx >= 0) { + int colon = json.indexOf(":", idx); + int q1 = json.indexOf("\"", colon + 1); + int q2 = json.indexOf("\"", q1 + 1); + if (q2 > q1) { + return cleanTag(json.substring(q1 + 1, q2)); + } + } + + return null; + } + + private String cleanTag(String tag) { + if (tag.startsWith("v") || tag.startsWith("V")) { + tag = tag.substring(1); + } + return tag.isEmpty() ? null : tag; + } + + // ========== 版本比较 ========== + // ★ 只要本地≠远程,就视为有更新 + + private boolean isNewer(String remote, String local) { + if (remote == null || local == null) return false; + // 版本比较:远程≠本地即视为有更新(用户即将推新版本) + return !remote.trim().equals(local.trim()); + } + + private int parseNum(String s) { + try { + return Integer.parseInt(s.replaceAll("[^0-9]", "")); + } catch (Exception e) { + return 0; + } + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 2103531..21a8d06 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: Sdf1_game -version: 1.0 +version: 1.4 main: Sdf1_game.Main api-version: '1.21' description: 快问快答+寻宝系统