领地门禁文案动态化 + 违禁品管理面板 + 二手交易商店/shopbuy
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m14s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s

- 门禁权限(door)文案按启停状态动态显示允许/禁止,CLI与GUI同步
- 违禁品管理面板:访客禁带物品配置、没收计数、退出领地按设置归还(默认不退)
- 新增/shopbuy二手商店:玩家自由上架定价(封底0债券)、商店底部入口、冻结账户拦截
This commit is contained in:
youpaishidifu
2026-10-11 17:27:53 +08:00
parent 54d520ab3d
commit f6ad2ca3f9
7 changed files with 863 additions and 10 deletions
+138 -3
View File
@@ -354,6 +354,9 @@ public class AreaCLIManager {
// ★ 新增:领地公告管理
p.sendMessage(clickableAction("📢", "领地公告(进出提示消息)",
"/protect cli announcement " + land.name + " 1"));
// ★ 新增:违禁品管理面板(2026-10-11)
p.sendMessage(clickableAction("🚫", "违禁品管理(禁带物品/没收计数/退出归还)",
"/protect cli confiscate " + land.name));
// ★ 新增:领地改名
long[] cooldown = areaProtect.getRenameCooldown(p);
String renameHint;
@@ -704,7 +707,8 @@ public class AreaCLIManager {
// ★ 2026-10-10 文案动态化:按启停状态显示"允许/禁止交互展示框"
perms.add(new PermItem("item_frame", land.denyItemFrame ? "禁止交互展示框" : "允许交互展示框", !land.denyItemFrame));
perms.add(new PermItem("redstone", "红石电路(中继器/比较器)", !land.denyRedstoneInteraction));
perms.add(new PermItem("door", "禁止门禁(门/按钮/压力板)", !land.denyDoorInteraction));
// ★ 2026-10-10 文案动态化:按启停状态显示"允许/禁止门禁交互"
perms.add(new PermItem("door", land.denyDoorInteraction ? "禁止门禁(门/按钮/压力板)" : "允许门禁(门/按钮/压力板)", !land.denyDoorInteraction));
perms.add(new PermItem("sign_edit", land.denySignEdit ? "禁止编辑告示牌" : "允许编辑告示牌", !land.denySignEdit));
perms.add(new PermItem("audio", "音频(音符盒/唱片机)", !land.denyNoteblockJukebox));
perms.add(new PermItem("lead", "拴绳使用", !land.denyLead));
@@ -748,7 +752,7 @@ public class AreaCLIManager {
case "all_effects": return "所有效果";
case "item_frame": return "展示框交互";
case "redstone": return "红石电路";
case "door": return "禁止门禁";
case "door": return "门禁交互";
case "sign_edit": return "告示牌编辑";
case "audio": return "音频";
case "lead": return "拴绳使用";
@@ -1169,7 +1173,7 @@ public class AreaCLIManager {
{"all_effects", "所有效果", "denyAllEffects"},
{"item_frame", "展示框交互", "denyItemFrame"},
{"redstone", "红石电路(中继器/比较器)", "denyRedstoneInteraction"},
{"door", "禁止门禁(门/按钮/压力板)", "denyDoorInteraction"},
{"door", "门禁交互", "denyDoorInteraction"},
{"sign_edit", "禁止编辑告示牌", "denySignEdit"},
{"audio", "音频(音符盒/唱片机)", "denyNoteblockJukebox"},
{"lead", "拴绳使用", "denyLead"},
@@ -1231,6 +1235,10 @@ public class AreaCLIManager {
if (field.equals("denyVillagerTrade")) {
name = landDefault ? "禁止村民交易" : "允许村民交易";
}
// ★ denyDoorInteraction显示名称(2026-10-10 按启停状态动态显示)
if (field.equals("denyDoorInteraction")) {
name = landDefault ? "禁止门禁交互(门/按钮/压力板)" : "允许门禁交互(门/按钮/压力板)";
}
// 如果有per-player覆盖,使用覆盖值;否则使用领地默认
if (playerPerms.containsKey(field)) {
@@ -2037,4 +2045,131 @@ public class AreaCLIManager {
showAnnouncementManagement(p, landName, 1);
return true;
}
// ==================== 违禁品管理面板(2026-10-11) ====================
/**
* ★ 违禁品管理面板(CLI)。
*
* <p>展示:没收计数、当前访客禁带物品列表、退出是否归还开关。
* 领地主/管理员可在此配置「访客禁止携带的物品」,并控制退出领地时
* 是否退还已没收物品(默认不退)。
*/
public void showConfiscateManagement(Player p, String landName) {
AreaProtection.AreaConfig land = areaProtect.getLand(landName);
if (land == null) {
p.sendMessage(Component.text("§c领地不存在: " + landName));
return;
}
// 权限检查
boolean isOwner = AreaProtection.samePlayer(p.getName(), land.owner);
boolean isAdmin = areaProtect.isAreaAdmin(p);
if (!isOwner && !isAdmin) {
p.sendMessage(Component.text("§c需要领地所有者或管理员权限"));
return;
}
p.sendMessage(header("违禁品管理: " + landName));
p.sendMessage(Component.text("§7配置访客进入领地时禁止携带的物品"));
p.sendMessage(Component.text("§7§l───────────────────────────────"));
// 没收计数
p.sendMessage(Component.text("§a累计没收次数: §f" + land.confiscateCount + " §7次"));
// 当前禁带物品列表
p.sendMessage(Component.text(""));
if (land.confiscateItems == null || land.confiscateItems.isEmpty()) {
p.sendMessage(Component.text("§7当前访客禁带物品: §c(空)"));
} else {
p.sendMessage(Component.text("§7当前访客禁带物品(§f" + land.confiscateItems.size() + "§7种):"));
for (int i = 0; i < land.confiscateItems.size(); i++) {
final int idx = i;
String item = land.confiscateItems.get(i);
p.sendMessage(Component.empty()
.append(Component.text("§f " + (i + 1) + ". " + item + " "))
.append(Component.text("§c[移除]")
.hoverEvent(HoverEvent.showText(Component.text("§e点击移除该禁带物品")))
.clickEvent(ClickEvent.runCommand(
"/protect cli confiscateremove " + landName + " " + (i + 1)))));
}
}
// 添加物品入口
p.sendMessage(Component.text(""));
p.sendMessage(clickableAction("➕", "添加禁带物品",
"/protect cli confiscateadd " + landName));
// 退出是否归还开关
p.sendMessage(Component.text(""));
String retState = land.returnOnLeave ? "§a开启" : "§c关闭";
p.sendMessage(Component.empty()
.append(Component.text("§a退出领地归还没收物: " + retState))
.append(Component.text(" §7(当前" + (land.returnOnLeave ? "开启,退出时退还" : "关闭,退出不退还") + ")")));
p.sendMessage(Component.empty()
.append(Component.text(land.returnOnLeave ? "§c[关闭归还] " : "§a[开启归还] "))
.hoverEvent(HoverEvent.showText(Component.text("§e切换:玩家离开领地时是否退还此前被没收的物品")))
.clickEvent(ClickEvent.runCommand(
"/protect cli confiscateret " + landName + " " + (land.returnOnLeave ? "off" : "on"))));
p.sendMessage(Component.text("§7§l───────────────────────────────"));
p.sendMessage(clickableAction("◀", "返回领地管理", "/protect cli manage " + landName));
p.sendMessage(Component.text("§7§l───────────────────────────────"));
}
/**
* 开始添加禁带物品(等待玩家聊天输入物品ID)
*/
public void startAddConfiscateItem(Player p, String landName) {
AreaProtection.AreaConfig land = areaProtect.getLand(landName);
if (land == null) { p.sendMessage(Component.text("§c领地不存在: " + landName)); return; }
boolean isOwner = AreaProtection.samePlayer(p.getName(), land.owner);
boolean isAdmin = areaProtect.isAreaAdmin(p);
if (!isOwner && !isAdmin) { p.sendMessage(Component.text("§c需要领地所有者或管理员权限")); return; }
areaProtect.setPendingEffectInput(p.getUniqueId(), landName, "confiscate_add", 3);
p.sendMessage(header("添加禁带物品: " + landName));
p.sendMessage(Component.text("§e请在聊天栏输入物品ID(如 diamond、minecraft:gold_ingot)"));
p.sendMessage(Component.text("§7输入 §c取消 §7放弃添加"));
}
/**
* 移除指定序号的禁带物品
*/
public void removeConfiscateItem(Player p, String landName, String indexStr) {
AreaProtection.AreaConfig land = areaProtect.getLand(landName);
if (land == null) { p.sendMessage(Component.text("§c领地不存在: " + landName)); return; }
boolean isOwner = AreaProtection.samePlayer(p.getName(), land.owner);
boolean isAdmin = areaProtect.isAreaAdmin(p);
if (!isOwner && !isAdmin) { p.sendMessage(Component.text("§c需要领地所有者或管理员权限")); return; }
int idx;
try { idx = Integer.parseInt(indexStr); } catch (Exception e) { p.sendMessage(Component.text("§c序号无效")); return; }
if (idx < 1 || idx > land.confiscateItems.size()) { p.sendMessage(Component.text("§c序号超出范围")); return; }
String removed = land.confiscateItems.remove(idx - 1);
areaProtect.saveAreaToDb(land);
p.sendMessage(Component.text("§a已移除禁带物品: §f" + removed));
}
/**
* 切换「退出领地是否归还没收物」开关
*/
public void toggleReturnOnLeave(Player p, String landName, String newState) {
AreaProtection.AreaConfig land = areaProtect.getLand(landName);
if (land == null) {
p.sendMessage(Component.text("§c领地不存在: " + landName));
return;
}
boolean isOwner = AreaProtection.samePlayer(p.getName(), land.owner);
boolean isAdmin = areaProtect.isAreaAdmin(p);
if (!isOwner && !isAdmin) {
p.sendMessage(Component.text("§c需要领地所有者或管理员权限"));
return;
}
if (newState.equalsIgnoreCase("on")) {
land.returnOnLeave = true;
p.sendMessage(Component.text("§a已开启:玩家离开领地时将退还没收的物品"));
} else {
land.returnOnLeave = false;
p.sendMessage(Component.text("§a已关闭:玩家离开领地时不退还没收的物品"));
}
areaProtect.saveAreaToDb(land);
}
}
+129 -2
View File
@@ -42,6 +42,7 @@ public class AreaGUIManager implements Listener {
private static final String T_EFFECTS_CLEAR = "§b§l单清效果";
private static final String T_EFFECTS_GIVE = "§b§l增益效果";
private static final String T_LAND_TRANSFER = "§6§l过户领地";
private static final String T_CONFISCATE = "§c§l违禁品管理 - ";
// 每页显示数量
private static final int PAGE_SIZE = 45;
@@ -266,6 +267,15 @@ public class AreaGUIManager implements Listener {
inv.setItem(33, createItem(Material.NAME_TAG, "§e§l领地改名",
renameLore, "", "§e点击输入新名字"));
// 违禁品管理(位置34,2026-10-11)
inv.setItem(34, createItem(Material.BARRIER, "§c§l违禁品管理",
"§7配置访客禁止携带的物品",
"§7进入回收 · 退出按设置归还",
"§7累计没收: §f" + land.confiscateCount + " §7次",
"§7退出归还: " + (land.returnOnLeave ? "§a开启" : "§c关闭"),
"",
"§e点击管理"));
// 返回按钮(位置48)
inv.setItem(48, createItem(Material.ARROW, "§c§l返回领地列表", ""));
@@ -544,6 +554,10 @@ public class AreaGUIManager implements Listener {
if (field.equals("denyVillagerTrade")) {
name = landDefault ? "禁止村民交易" : "允许村民交易";
}
// ★ denyDoorInteraction显示名称(2026-10-10 按启停状态动态显示)
if (field.equals("denyDoorInteraction")) {
name = landDefault ? "禁止门禁交互" : "允许门禁交互";
}
// ★ 对于 allowPickup/allowDrop,enabled=true 表示"允许操作"(即 allow=true)
// 其他权限,enabled = true 表示"允许操作"(即 deny=false)
@@ -706,7 +720,8 @@ public class AreaGUIManager implements Listener {
// ★ 2026-10-10 文案动态化:按启停状态显示"允许/禁止交互展示框"
perms.add(new PermEntry(land.denyItemFrame ? "禁止交互展示框" : "允许交互展示框", !land.denyItemFrame));
perms.add(new PermEntry("红石电路(按钮/压力板/中继器/比较器)", !land.denyRedstoneInteraction));
perms.add(new PermEntry("门禁交互(门/栅栏门)", !land.denyDoorInteraction));
// ★ 2026-10-10 文案动态化:按启停状态显示"允许/禁止门禁交互"
perms.add(new PermEntry(land.denyDoorInteraction ? "禁止门禁交互(门/栅栏门)" : "允许门禁交互(门/栅栏门)", !land.denyDoorInteraction));
perms.add(new PermEntry("音频(音符盒/唱片机)", !land.denyNoteblockJukebox));
perms.add(new PermEntry("拴绳使用", !land.denyLead));
perms.add(new PermEntry(land.denyCropHarvest ? "禁用农作物收获" : "启用农作物收获", !land.denyCropHarvest));
@@ -770,7 +785,7 @@ public class AreaGUIManager implements Listener {
case "所有效果": land.denyAllEffects = !land.denyAllEffects; break;
case "禁止展示框": case "允许交互展示框": case "禁止交互展示框": land.denyItemFrame = !land.denyItemFrame; break;
case "红石电路(按钮/压力板/中继器/比较器)": land.denyRedstoneInteraction = !land.denyRedstoneInteraction; break;
case "门禁交互(门/栅栏门)": land.denyDoorInteraction = !land.denyDoorInteraction; break;
case "门禁交互(门/栅栏门)": case "禁止门禁交互(门/栅栏门)": case "允许门禁交互(门/栅栏门)": land.denyDoorInteraction = !land.denyDoorInteraction; break;
case "音频(音符盒/唱片机)": land.denyNoteblockJukebox = !land.denyNoteblockJukebox; break;
case "拴绳使用": land.denyLead = !land.denyLead; break;
case "农作物收获": land.denyCropHarvest = !land.denyCropHarvest; break;
@@ -1002,6 +1017,9 @@ public class AreaGUIManager implements Listener {
openLandSettings(p, landName);
} else if (raw == 16) {
openEffectsManagement(p, landName, 1);
} else if (raw == 34) {
// ★ 违禁品管理面板(2026-10-11)
openConfiscatePanel(p, landName);
} else if (raw == 32) {
// 过户领地
openTransferLandPanel(p, landName);
@@ -1059,6 +1077,52 @@ public class AreaGUIManager implements Listener {
return;
}
// ★ 违禁品管理面板(2026-10-11)
if (title.startsWith(T_CONFISCATE)) {
event.setCancelled(true);
String landName = title.substring(T_CONFISCATE.length()).trim();
if (!event.isLeftClick()) return;
AreaProtection.AreaConfig land = areaProtect.getLand(landName);
if (land == null) { p.closeInventory(); return; }
int[] slots = {10,11,12,13,14,15,16, 19,20,21,22,23,24,25, 28,29,30,31,32,33,34};
// 判断点击是否命中某个禁带物品
int hitIdx = -1;
for (int i = 0; i < slots.length && i < land.confiscateItems.size(); i++) {
if (slots[i] == raw) { hitIdx = i; break; }
}
if (hitIdx >= 0) {
String removed = land.confiscateItems.remove(hitIdx);
areaProtect.saveAreaToDb(land);
p.sendMessage("§a§l[违禁品] §f已移除禁带物品: §e" + removed);
openConfiscatePanel(p, landName);
return;
}
if (raw == 22) {
// 切换退出归还开关
land.returnOnLeave = !land.returnOnLeave;
areaProtect.saveAreaToDb(land);
p.sendMessage(land.returnOnLeave
? "§a§l[违禁品] §f已开启:玩家离开领地时退还没收物"
: "§c§l[违禁品] §f已关闭:玩家离开领地时不退还没收物");
openConfiscatePanel(p, landName);
return;
}
if (raw == 40) {
// 添加禁带物品 → 关闭GUI转聊天输入
p.closeInventory();
areaProtect.setPendingEffectInput(p.getUniqueId(), landName, "confiscate_add", 1);
p.sendMessage("§e§l[违禁品] §f请在聊天栏输入物品ID(如 diamond、gold_ingot),输入 §c取消 §7放弃");
return;
}
if (raw == 48) {
openLandManage(p, landName);
return;
}
return;
}
// 成员列表
if (title.startsWith(T_MEMBER_LIST)) {
event.setCancelled(true);
@@ -2309,6 +2373,69 @@ public class AreaGUIManager implements Listener {
p.openInventory(inv);
}
/**
* ★ 违禁品管理面板(2026-10-11)。
*
* <p>展示没收计数、当前访客禁带物品列表(点击移除)、添加入口、
* 以及「退出领地是否归还没收物」开关(默认关闭=不退)。
*/
public void openConfiscatePanel(Player p, String landName) {
managingLand.put(p.getUniqueId(), landName);
Inventory inv = Bukkit.createInventory(null, 54, T_CONFISCATE + landName);
fillBg(inv);
AreaProtection.AreaConfig land = areaProtect.getLand(landName);
if (land == null) {
p.sendMessage("§c领地不存在");
p.closeInventory();
return;
}
// 没收计数(位置4)
inv.setItem(4, createItem(Material.HOPPER, "§e§l没收计数",
"§7该领地累计没收违禁品次数",
"§7累计: §f" + land.confiscateCount + " §7次"));
// 退出归还开关(位置22)
inv.setItem(22, createItem(land.returnOnLeave ? Material.LIME_DYE : Material.GRAY_DYE,
land.returnOnLeave ? "§a§l退出归还: 开启" : "§c§l退出归还: 关闭",
"§7玩家离开领地时是否退还没收物",
"§7当前: " + (land.returnOnLeave ? "§a开启,退出时退还" : "§c关闭,退出不退还"),
"",
"§e点击切换"));
// 禁带物品列表(位置 10-16, 19-25, 28-34)
int[] slots = {10,11,12,13,14,15,16, 19,20,21,22,23,24,25, 28,29,30,31,32,33,34};
java.util.List<String> items = land.confiscateItems;
for (int i = 0; i < items.size() && i < slots.length; i++) {
final int idx = i;
String item = items.get(i);
org.bukkit.Material mat = org.bukkit.Material.matchMaterial(item);
inv.setItem(slots[i], createItem(
mat != null ? mat : Material.BARRIER,
"§c" + item,
"§7访客禁止携带此物品",
"",
"§e点击移除"));
}
// 添加物品按钮(位置40)
inv.setItem(40, createItem(Material.EMERALD, "§a§l添加禁带物品",
"§7在聊天栏输入物品ID",
"§7示例: diamond、gold_ingot",
"",
"§e点击添加"));
// 当前数量提示(位置49)
inv.setItem(49, createItem(Material.PAPER, "§7§l共 " + items.size() + " 种禁带物品",
"§7列表已满时请先移除"));
// 返回按钮(位置48)
inv.setItem(48, createItem(Material.ARROW, "§c§l返回管理领地", ""));
p.openInventory(inv);
}
/**
* 处理过户领地GUI点击事件
*/
+155 -4
View File
@@ -103,6 +103,10 @@ public class AreaProtection implements Listener {
private final Map<java.util.UUID, String> pendingAddMemberInput = new ConcurrentHashMap<>();
// ★ 效果管理待处理输入:UUID → [landName, inputType]
public final Map<java.util.UUID, String[]> pendingEffectInput = new ConcurrentHashMap<>();
// ★ 违禁品没收暂存(2026-10-11 违禁品管理面板):UUID → (领地名 → 被没收物品)
// 玩家进入领地被没收时暂存;退出领地时若 returnOnLeave=true 则归还,否则清空(不退)
public final Map<java.util.UUID, java.util.Map<String, java.util.List<org.bukkit.inventory.ItemStack>>> confiscatedStore
= new ConcurrentHashMap<>();
// ★ 过户领地待输入新所有者:UUID → [landName]
public final Map<java.util.UUID, String[]> pendingTransferInput = new ConcurrentHashMap<>();
// ★ 用户组配置编辑待输入:UUID → [groupName, field("price"|"maxlands"|"priority")]
@@ -1252,6 +1256,8 @@ public class AreaProtection implements Listener {
try { ac.denyWax = rs.getInt("deny_wax") == 1; } catch (Exception ignored) {}
try { ac.denyVillagerTrade = rs.getInt("deny_villager_trade") == 1; } catch (Exception ignored) {}
try { ac.badEffects = parseEffectsString(rs.getString("bad_effects")); } catch (Exception ignored) { ac.badEffects = new ArrayList<>(); }
try { ac.confiscateCount = rs.getInt("confiscate_count"); } catch (Exception ignored) {}
try { ac.returnOnLeave = rs.getInt("return_on_leave") == 1; } catch (Exception ignored) {}
try { ac.warpX = rs.getDouble("warp_x"); } catch (Exception ignored) {}
try { ac.warpY = rs.getDouble("warp_y"); } catch (Exception ignored) {}
try { ac.warpZ = rs.getDouble("warp_z"); } catch (Exception ignored) {}
@@ -1556,6 +1562,9 @@ public class AreaProtection implements Listener {
// ★ 村民交易权限列(2026-10-10:领地主控制玩家能否与领地内村民交易)
try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_villager_trade INTEGER DEFAULT 0"); } catch (Exception ignored) {}
try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN bad_effects TEXT DEFAULT ''"); } catch (Exception ignored) {}
// ★ 违禁品管理面板(2026-10-11):没收计数 + 退出是否归还
try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN confiscate_count INTEGER DEFAULT 0"); } catch (Exception ignored) {}
try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN return_on_leave INTEGER DEFAULT 0"); } catch (Exception ignored) {}
// ★ 全局配置默认值
try {
@@ -3532,6 +3541,9 @@ public class AreaProtection implements Listener {
showBorderBrief(p, oldAc);
}
// ★ 违禁品归还(2026-10-11 违禁品管理面板):按 returnOnLeave 决定是否退还
handleConfiscateReturn(p, oldArea);
// 收标
List<PotionEffectType> marked =
playerMarkedEffects.remove(uid);
@@ -4850,6 +4862,8 @@ public class AreaProtection implements Listener {
if (fromArea != null) {
showBorderBrief(p, fromArea);
}
// ★ 违禁品归还(2026-10-11 违禁品管理面板):按 returnOnLeave 决定是否退还
handleConfiscateReturn(p, fromName);
}
// ===== 进入新区域 =====
@@ -7483,6 +7497,40 @@ public class AreaProtection implements Listener {
}
plugin.areaCLIManager.showAnnouncementManagement(p, args[2], annPage);
break;
case "confiscate":
// ★ 违禁品管理面板(2026-10-11)
if (args.length < 3) {
p.sendMessage("§c用法: /protect cli confiscate <领地名>");
break;
}
plugin.areaCLIManager.showConfiscateManagement(p, args[2]);
break;
case "confiscateret":
// ★ 切换「退出领地是否归还没收物」
if (args.length < 4) {
p.sendMessage("§c用法: /protect cli confiscateret <领地名> on/off");
break;
}
plugin.areaCLIManager.toggleReturnOnLeave(p, args[2], args[3]);
plugin.areaCLIManager.showConfiscateManagement(p, args[2]);
break;
case "confiscateadd":
// ★ 添加禁带物品(交互式输入)
if (args.length < 3) {
p.sendMessage("§c用法: /protect cli confiscateadd <领地名>");
break;
}
plugin.areaCLIManager.startAddConfiscateItem(p, args[2]);
break;
case "confiscateremove":
// ★ 移除指定禁带物品
if (args.length < 4) {
p.sendMessage("§c用法: /protect cli confiscateremove <领地名> <序号>");
break;
}
plugin.areaCLIManager.removeConfiscateItem(p, args[2], args[3]);
plugin.areaCLIManager.showConfiscateManagement(p, args[2]);
break;
case "rename":
// ★ 领地改名(交互式输入)
if (args.length < 3) {
@@ -10616,8 +10664,8 @@ public class AreaProtection implements Listener {
+ "confiscate_msg, enable_announce, announce_template, txt_content, created_at, "
+ "deny_thrown_projectiles, deny_glowing, deny_redstone_interaction, deny_door_interaction, "
+ "deny_noteblock_jukebox, deny_lead, deny_crop_harvest, deny_wool_shear, deny_animal_feeding, "
+ "warp_x, warp_y, warp_z, warp_yaw, warp_pitch, warp_world, deny_container, deny_mob_attack, deny_sign_edit, is_public_building, allow_visitor_teleport, deny_farmland_trample, deny_ender_teleport, create_cost, deny_spawn_egg, deny_wax, bad_effects, deny_villager_trade) "
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
+ "warp_x, warp_y, warp_z, warp_yaw, warp_pitch, warp_world, deny_container, deny_mob_attack, deny_sign_edit, is_public_building, allow_visitor_teleport, deny_farmland_trample, deny_ender_teleport, create_cost, deny_spawn_egg, deny_wax, bad_effects, deny_villager_trade, confiscate_count, return_on_leave) "
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
+ "ON CONFLICT(name) DO UPDATE SET "
+ "owner=excluded.owner, world=excluded.world, x1=excluded.x1, z1=excluded.z1, x2=excluded.x2, z2=excluded.z2, y_min=excluded.y_min, y_max=excluded.y_max, "
+ "confiscate_items=excluded.confiscate_items, deny_use_items=excluded.deny_use_items, give_effects=excluded.give_effects, clear_effects=excluded.clear_effects, clear_all_bad=excluded.clear_all_bad, "
@@ -10630,7 +10678,7 @@ public class AreaProtection implements Listener {
+ "confiscate_msg=excluded.confiscate_msg, enable_announce=excluded.enable_announce, announce_template=excluded.announce_template, txt_content=excluded.txt_content, created_at=excluded.created_at, "
+ "deny_thrown_projectiles=excluded.deny_thrown_projectiles, deny_glowing=excluded.deny_glowing, deny_redstone_interaction=excluded.deny_redstone_interaction, deny_door_interaction=excluded.deny_door_interaction, "
+ "deny_noteblock_jukebox=excluded.deny_noteblock_jukebox, deny_lead=excluded.deny_lead, deny_crop_harvest=excluded.deny_crop_harvest, deny_wool_shear=excluded.deny_wool_shear, deny_animal_feeding=excluded.deny_animal_feeding, "
+ "warp_x=excluded.warp_x, warp_y=excluded.warp_y, warp_z=excluded.warp_z, warp_yaw=excluded.warp_yaw, warp_pitch=excluded.warp_pitch, warp_world=excluded.warp_world, deny_container=excluded.deny_container, deny_mob_attack=excluded.deny_mob_attack, deny_sign_edit=excluded.deny_sign_edit, is_public_building=excluded.is_public_building, allow_visitor_teleport=excluded.allow_visitor_teleport, deny_farmland_trample=excluded.deny_farmland_trample, deny_ender_teleport=excluded.deny_ender_teleport, create_cost=excluded.create_cost, deny_spawn_egg=excluded.deny_spawn_egg, deny_wax=excluded.deny_wax, bad_effects=excluded.bad_effects, deny_villager_trade=excluded.deny_villager_trade");
+ "warp_x=excluded.warp_x, warp_y=excluded.warp_y, warp_z=excluded.warp_z, warp_yaw=excluded.warp_yaw, warp_pitch=excluded.warp_pitch, warp_world=excluded.warp_world, deny_container=excluded.deny_container, deny_mob_attack=excluded.deny_mob_attack, deny_sign_edit=excluded.deny_sign_edit, is_public_building=excluded.is_public_building, allow_visitor_teleport=excluded.allow_visitor_teleport, deny_farmland_trample=excluded.deny_farmland_trample, deny_ender_teleport=excluded.deny_ender_teleport, create_cost=excluded.create_cost, deny_spawn_egg=excluded.deny_spawn_egg, deny_wax=excluded.deny_wax, bad_effects=excluded.bad_effects, deny_villager_trade=excluded.deny_villager_trade, confiscate_count=excluded.confiscate_count, return_on_leave=excluded.return_on_leave");
stmt.setString(1, ac.name);
stmt.setString(2, ac.owner != null ? ac.owner : "");
@@ -10706,6 +10754,8 @@ public class AreaProtection implements Listener {
stmt.setInt(72, ac.denyWax ? 1 : 0);
stmt.setString(73, effectsToString(ac.badEffects));
stmt.setInt(74, ac.denyVillagerTrade ? 1 : 0);
stmt.setInt(75, ac.confiscateCount);
stmt.setInt(76, ac.returnOnLeave ? 1 : 0);
stmt.executeUpdate();
stmt.close();
// ★ 领地设置变更:立即触发PHP同步(防抖10秒)
@@ -10927,7 +10977,8 @@ public class AreaProtection implements Listener {
"deny_crop_harvest", "deny_wool_shear", "deny_animal_feeding",
"deny_container", "deny_mob_attack", "deny_fluid",
"is_public_building", "allow_visitor_teleport",
"deny_spawn_egg", "deny_wax", "deny_villager_trade"
"deny_spawn_egg", "deny_wax", "deny_villager_trade",
"confiscate_count", "return_on_leave"
));
if (!allowedFields.contains(field)) {
plugin.getLogger().warning("[防护] PHP端更新未知字段: " + field);
@@ -11116,6 +11167,10 @@ public class AreaProtection implements Listener {
case "deny_mob_attack": ac.denyMobAttack = "1".equals(value); break;
case "deny_item_frame": ac.denyItemFrame = "1".equals(value); break;
case "deny_villager_trade": ac.denyVillagerTrade = "1".equals(value); break;
case "return_on_leave": ac.returnOnLeave = "1".equals(value); break;
case "confiscate_count":
try { ac.confiscateCount = Integer.parseInt(value); } catch (Exception ignored) {}
break;
case "deny_entity_interact": ac.denyEntityInteract = "1".equals(value); break;
case "deny_pickup": ac.allowPickup = "0".equals(value); break; // deny_pickup 反向
case "deny_drop": ac.allowDrop = "0".equals(value); break; // deny_drop 反向
@@ -11797,6 +11852,38 @@ public class AreaProtection implements Listener {
p.sendMessage(Component.text("§a✓ 领地已改名: §f" + landName + " §a→ §f" + newName));
if (plugin.areaCLIManager != null) plugin.areaCLIManager.showLandManage(p, newName, 1);
return true;
} else if ("confiscate_add".equals(inputType)) {
// ★ 违禁品管理面板:添加访客禁带物品(2026-10-11)
if (message.trim().equals("取消")) {
p.sendMessage(Component.text("§e已取消添加,保持原样"));
if (plugin.areaCLIManager != null) plugin.areaCLIManager.showConfiscateManagement(p, landName);
return true;
}
String itemId = message.trim();
if (itemId.isEmpty()) {
p.sendMessage(Component.text("§c§l[添加禁带物品] §f物品ID不能为空"));
pendingEffectInput.put(p.getUniqueId(), new String[]{landName, "confiscate_add"});
return true;
}
// ★ 验证物品ID是否有效(Material.matchMaterial)
org.bukkit.Material mat = org.bukkit.Material.matchMaterial(itemId);
if (mat == null) {
p.sendMessage(Component.text("§c§l[添加禁带物品] §f无效的物品ID: §e" + itemId));
p.sendMessage(Component.text("§7示例: diamond、minecraft:gold_ingot、netherite_sword"));
pendingEffectInput.put(p.getUniqueId(), new String[]{landName, "confiscate_add"});
return true;
}
// 统一为小写标准名,避免重复
String stdId = mat.name().toLowerCase();
if (land.confiscateItems.contains(stdId)) {
p.sendMessage(Component.text("§e§l[添加禁带物品] §f物品 §e" + stdId + " §f已在列表中"));
} else {
land.confiscateItems.add(stdId);
saveAreaToDb(land);
p.sendMessage(Component.text("§a§l[添加禁带物品] §f已添加访客禁带物品: §e" + stdId));
}
if (plugin.areaCLIManager != null) plugin.areaCLIManager.showConfiscateManagement(p, landName);
return true;
}
// 刷新GUI,回到原来的subPage(必须切主线程)
@@ -12511,6 +12598,8 @@ public class AreaProtection implements Listener {
int totalRemoved = 0;
String confiscatedNames = "";
// ★ 违禁品管理面板(2026-10-11):捕获被没收的物品实体,供退出领地时归还
java.util.List<ItemStack> captured = new ArrayList<>();
for (String itemId : allItems) {
// 过滤掉区域名和无效ID
@@ -12525,6 +12614,7 @@ public class AreaProtection implements Listener {
for (int i = 0; i < all.length; i++) {
if (all[i] != null && all[i].getType() == mat) {
count += all[i].getAmount();
captured.add(all[i].clone());
all[i] = null;
}
}
@@ -12533,6 +12623,7 @@ public class AreaProtection implements Listener {
ItemStack off = p.getInventory().getItemInOffHand();
if (off != null && off.getType() == mat) {
count += off.getAmount();
captured.add(off.clone());
p.getInventory().setItemInOffHand(null);
}
@@ -12546,6 +12637,17 @@ public class AreaProtection implements Listener {
}
if (totalRemoved > 0) {
// ★ 没收计数 +1 并落库(领地主可在违禁品面板查看累计没收次数)
ac.confiscateCount++;
saveAreaToDb(ac);
// ★ 暂存被没收物品:退出领地时按 returnOnLeave 决定是否归还
java.util.Map<String, java.util.List<ItemStack>> byPlayer =
confiscatedStore.computeIfAbsent(p.getUniqueId(),
k -> new ConcurrentHashMap<>());
java.util.List<ItemStack> store = byPlayer.computeIfAbsent(ac.name,
k -> new ArrayList<>());
store.addAll(captured);
String msg = (ac.confiscateMsg != null
&& !ac.confiscateMsg.isEmpty())
? ac.confiscateMsg
@@ -12586,6 +12688,51 @@ public class AreaProtection implements Listener {
plugin.getLogger().info("[防护] 已清空所有黑名单");
}
/**
* ★ 违禁品归还(2026-10-11 违禁品管理面板)。
*
* <p>玩家离开领地时调用:若该领地 {@code returnOnLeave=true},把此前
* {@link #handleConfiscate} 暂存的没收物品退还给玩家(背包满则掉落脚下);
* 若为 false(默认),直接清空暂存(不退还)。两种情况都会清掉暂存记录。
*/
public void handleConfiscateReturn(Player p, String areaName) {
if (areaName == null) return;
java.util.Map<String, java.util.List<ItemStack>> byPlayer =
confiscatedStore.get(p.getUniqueId());
if (byPlayer == null) return;
java.util.List<ItemStack> store = byPlayer.remove(areaName);
if (byPlayer.isEmpty()) confiscatedStore.remove(p.getUniqueId());
if (store == null || store.isEmpty()) return;
AreaConfig ac = areas.get(areaName);
if (ac == null || !ac.returnOnLeave) {
// 默认不退还:仅清空暂存
return;
}
int returned = 0;
StringBuilder names = new StringBuilder();
for (ItemStack it : store) {
if (it == null || it.getType().isAir()) continue;
java.util.HashMap<Integer, ItemStack> left =
p.getInventory().addItem(it);
if (!left.isEmpty()) {
// 背包放不下:剩余部分掉落脚下
for (ItemStack drop : left.values()) {
p.getWorld().dropItemNaturally(p.getLocation(), drop);
}
}
returned += it.getAmount();
if (names.length() > 0) names.append("§7、§e");
names.append("§e").append(matname(it.getType()))
.append(" x").append(it.getAmount());
}
if (returned > 0) {
p.sendMessage("§a§l[区域防护] §f你离开领地 §e" + areaName
+ "§f,已退还没收物品: " + names);
}
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onEntityDamage(
org.bukkit.event.entity.EntityDamageByEntityEvent e) {
@@ -13389,6 +13536,10 @@ public class AreaProtection implements Listener {
public int x1, z1, x2, z2;
public int yMin = 0, yMax = 255;
public List<String> confiscateItems = new ArrayList<>();
// ★ 违禁品没收计数:领地累计没收次数(2026-10-11 违禁品管理面板)
public int confiscateCount = 0;
// ★ 退出领地是否归还没收物(默认 false=不退,由领地主控制)
public boolean returnOnLeave = false;
public List<String> denyUseItems = new ArrayList<>();
public List<String> clearEffects = new ArrayList<>(); // 清除单个效果名
public boolean clearAllBadEffects = false; // 清除所有负面+中性
+16
View File
@@ -807,6 +807,8 @@ public class GUIManager implements Listener {
if ("§a§l购物车".equals(title)
|| "§c§l退款中心".equals(title)
|| "§6§l商店".equals(title)
|| "§6§l二手商店".equals(title)
|| "§6§l我的二手上架".equals(title)
|| title.startsWith("§e§l流水记录")
|| title.startsWith("§e§l转账记录")
|| title.startsWith("§d§l余额操作")) {
@@ -970,6 +972,20 @@ public class GUIManager implements Listener {
return;
}
// ===== 二手商店(2026-10-11 /shopbuy)=====
if ("§6§l二手商店".equals(title)) {
event.setCancelled(true);
plugin.getShopManager().handleSecondHandClick(
p, raw, event.isLeftClick());
return;
}
if ("§6§l我的二手上架".equals(title)) {
event.setCancelled(true);
plugin.getShopManager().handleSecondHandMineClick(
p, raw, event.isLeftClick());
return;
}
// ===== 退款中心 → 重定向订单中心 =====
if ("§c§l退款中心".equals(title)) {
event.setCancelled(true);
+14
View File
@@ -7732,6 +7732,20 @@ public class Main extends JavaPlugin
return true;
}
// ===== /shopbuy 二手交易商店(2026-10-11)=====
if (cmd.getName().equalsIgnoreCase("shopbuy")) {
if (!(sender instanceof Player)) {
sender.sendMessage("§c仅玩家可使用");
return true;
}
if (shopManager != null) {
shopManager.openSecondHandShop((Player) sender, 0);
} else {
sender.sendMessage("§c商店系统未加载");
}
return true;
}
// ===== /tempban 临时封禁(名字或IP,DurationParser 解析时长) =====
if (cmd.getName().equalsIgnoreCase("tempban")) {
doTempBan(sender, "tempban", args);
+408 -1
View File
@@ -61,6 +61,16 @@ public class ShopManager implements Listener {
new HashSet<>();
private final Map<UUID, String> pendingCouponCode =
new HashMap<>();
// ===== 二手交易商店(2026-10-11 /shopbuy)=====
private final List<SecondHandListing> secondHandListings =
new ArrayList<>();
// 待定价上架:玩家UUID → 已暂存的物品(等待聊天输入售价)
private final Map<UUID, ItemStack> pendingPublishItem =
new HashMap<>();
private final Set<UUID> publishListening =
new HashSet<>();
// 每人同时最多上架数
private static final int SH_MAX_PER_PLAYER = 9;
// ===== 物品NBT数据 =====
private final Map<String, String> itemNbtData =
new HashMap<>();
@@ -188,6 +198,15 @@ public class ShopManager implements Listener {
int quantity;
}
// ===== 二手交易商店:一条玩家上架的商品(2026-10-11)=====
public static class SecondHandListing {
long id; // 自增ID,用于唯一标识与下架
String seller; // 上架玩家名
int price; // 售价(债券),封底 0
String itemB64; // 完整 ItemStack 的 Base64 序列化
long createdAt; // 上架时间戳(毫秒)
}
private static final int PAGE_SIZE = 28;
private static final int[] ITEM_SLOTS = {
@@ -203,6 +222,7 @@ public class ShopManager implements Listener {
loadMaxSellConfig();
startHourlyTask();
loadItemNbtData();
loadSecondHand();
startCleanupTask();
}
// ===== 切换购物车模式 =====
@@ -2950,7 +2970,15 @@ public class ShopManager implements Listener {
g.setItem(53, mkItem(Material.ARROW,
"§7返回"));
// ★ 二手商店入口(2026-10-11 /shopbuy)- slot 51
g.setItem(51, mkItem(Material.EMERALD,
"§6§l二手商店",
"§7玩家自由上架、自由定价",
"§7封底价格: §60 §7枚债券",
"",
"§a点击进入"));
p.openInventory(g);
}
@@ -3251,6 +3279,11 @@ public class ShopManager implements Listener {
openCouponInput(p);
return true;
}
// ★ 二手商店入口(2026-10-11)
if (raw == 51) {
openSecondHandShop(p, 0);
return true;
}
if (raw == 49) { plugin.getOrderManager().openOrderCenter(p); return true;}
int idx = slotToIndex(raw);
if (idx < 0 || idx >= categories.size())
@@ -3464,4 +3497,378 @@ public class ShopManager implements Listener {
}
return it;
}
// ==================== 二手交易商店(2026-10-11 /shopbuy) ====================
private File secondHandFile() {
File dir = new File(plugin.getDataFolder(), "shop");
if (!dir.exists()) dir.mkdirs();
return new File(dir, "secondhand.txt");
}
/** 载入二手商品(每行: id|seller|price|createdAt|base64) */
private void loadSecondHand() {
secondHandListings.clear();
File f = secondHandFile();
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;
String[] parts = line.split("\\|", 5);
if (parts.length < 5) continue;
SecondHandListing l = new SecondHandListing();
try {
l.id = Long.parseLong(parts[0]);
l.price = Integer.parseInt(parts[2]);
l.createdAt = Long.parseLong(parts[3]);
} catch (Exception e) { continue; }
l.seller = parts[1];
l.itemB64 = parts[4];
secondHandListings.add(l);
}
} catch (Exception e) {
plugin.getLogger().warning("[二手商店] 载入失败: " + e.getMessage());
}
}
/** 保存二手商品 */
private void saveSecondHand() {
try (BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream(secondHandFile()), StandardCharsets.UTF_8))) {
bw.write("# id|seller|price|createdAt|base64");
bw.newLine();
for (SecondHandListing l : secondHandListings) {
bw.write(l.id + "|" + l.seller + "|" + l.price
+ "|" + l.createdAt + "|" + l.itemB64);
bw.newLine();
}
} catch (Exception e) {
plugin.getLogger().warning("[二手商店] 保存失败: " + e.getMessage());
}
}
private long nextSecondHandId() {
long max = 0;
for (SecondHandListing l : secondHandListings) {
if (l.id > max) max = l.id;
}
return max + 1;
}
// ===== 打开二手商店主界面 =====
public void openSecondHandShop(Player p, int page) {
Inventory g = Bukkit.createInventory(null, 54, "§6§l二手商店");
int total = (int) Math.ceil((double) secondHandListings.size() / PAGE_SIZE);
if (total < 1) total = 1;
if (page < 0) page = 0;
if (page >= total) page = total - 1;
secondHandPages.put(p.getUniqueId(), page);
int start = page * PAGE_SIZE;
int end = Math.min(start + PAGE_SIZE, secondHandListings.size());
for (int i = start; i < end; i++) {
int idx = i - start;
if (idx >= ITEM_SLOTS.length) break;
SecondHandListing l = secondHandListings.get(i);
ItemStack is = base64ToItem(l.itemB64);
if (is == null) continue;
ItemMeta im = is.getItemMeta();
if (im != null) {
im.setLore(Arrays.asList(
"§7卖家: §f" + l.seller,
"§7售价: §6" + l.price + " §7枚债券",
"§7上架时间: §f" + new SimpleDateFormat("MM-dd HH:mm")
.format(new Date(l.createdAt)),
"",
"§a左键购买"));
is.setItemMeta(im);
}
g.setItem(ITEM_SLOTS[idx], is);
}
g.setItem(4, mkItem(Material.BOOK, "§e§l二手商店",
"§7共 §f" + secondHandListings.size() + " §7件商品",
"§7玩家自由上架,自由定价"));
g.setItem(45, mkItem(Material.EMERALD, "§a§l上架物品",
"§7将主手物品上架出售",
"§7封底价格: §60 §7枚债券",
"",
plugin.getBonds().isFrozen(p.getName())
? "§c债券账户被冻结,不可上架"
: "§e点击上架"));
g.setItem(46, mkItem(Material.CHEST, "§e§l我的上架",
"§7查看并管理自己上架的商品",
"", "§e点击进入"));
g.setItem(48, page > 0
? mkItem(Material.ARROW, "§7上一页")
: mkItem(Material.BLACK_STAINED_GLASS_PANE, " "));
g.setItem(49, mkItem(Material.PAPER, "§7" + (page + 1) + "/" + total));
g.setItem(50, page < total - 1
? mkItem(Material.ARROW, "§7下一页")
: mkItem(Material.BLACK_STAINED_GLASS_PANE, " "));
g.setItem(53, mkItem(Material.ARROW, "§7返回"));
p.openInventory(g);
}
// ===== 打开我的上架 =====
public void openSecondHandMine(Player p) {
Inventory g = Bukkit.createInventory(null, 54, "§6§l我的二手上架");
String me = p.getName();
int si = 0;
for (SecondHandListing l : secondHandListings) {
if (!me.equals(l.seller)) continue;
if (si >= ITEM_SLOTS.length) break;
ItemStack is = base64ToItem(l.itemB64);
if (is == null) continue;
ItemMeta im = is.getItemMeta();
if (im != null) {
im.setLore(Arrays.asList(
"§7售价: §6" + l.price + " §7枚债券",
"",
"§c左键下架(退还物品)"));
is.setItemMeta(im);
}
g.setItem(ITEM_SLOTS[si], is);
si++;
}
g.setItem(4, mkItem(Material.BOOK, "§e§l我的上架",
"§7共 §f" + si + " §7件商品"));
g.setItem(49, mkItem(Material.ARROW, "§7返回二手商店"));
p.openInventory(g);
}
// ===== 二手商店主界面点击 =====
public boolean handleSecondHandClick(Player p, int raw, boolean left) {
if (raw == 53) { openShopMain(p); return true; }
if (raw == 45) { startSecondHandPublish(p); return true; }
if (raw == 46) { openSecondHandMine(p); return true; }
if (raw == 48) {
int pg = getSecondHandPage(p.getUniqueId());
openSecondHandShop(p, pg - 1);
return true;
}
if (raw == 50) {
int pg = getSecondHandPage(p.getUniqueId());
openSecondHandShop(p, pg + 1);
return true;
}
if (!left) return true;
int idx = slotToIndex(raw);
if (idx < 0) return true;
int pg = getSecondHandPage(p.getUniqueId());
int actual = idx + pg * PAGE_SIZE;
if (actual < 0 || actual >= secondHandListings.size()) return true;
buySecondHand(p, secondHandListings.get(actual));
return true;
}
// ===== 我的上架点击 =====
public boolean handleSecondHandMineClick(Player p, int raw, boolean left) {
if (raw == 49) { openSecondHandShop(p, 0); return true; }
if (!left) return true;
int idx = slotToIndex(raw);
if (idx < 0) return true;
String me = p.getName();
int count = 0;
SecondHandListing target = null;
for (SecondHandListing l : secondHandListings) {
if (!me.equals(l.seller)) continue;
if (count == idx) { target = l; break; }
count++;
}
if (target == null) return true;
// 下架:退还物品
ItemStack is = base64ToItem(target.itemB64);
if (is != null) {
HashMap<Integer, ItemStack> left2 = p.getInventory().addItem(is);
for (ItemStack drop : left2.values()) {
p.getWorld().dropItemNaturally(p.getLocation(), drop);
}
}
secondHandListings.remove(target);
saveSecondHand();
p.sendMessage("§a§l[二手商店] §f已下架并退还物品");
openSecondHandMine(p);
return true;
}
// ===== 记录/读取二手商店页码 =====
private final Map<UUID, Integer> secondHandPages = new HashMap<>();
private int getSecondHandPage(UUID uuid) {
return secondHandPages.getOrDefault(uuid, 0);
}
// ===== 发布:拿主手物品 → 聊天输入售价 =====
public void startSecondHandPublish(Player p) {
if (plugin.getBonds().isFrozen(p.getName())) {
p.sendMessage("§c§l[二手商店] §f债券账户被冻结,不可上架/出售商品");
return;
}
// 统计该玩家当前上架数
int mine = 0;
for (SecondHandListing l : secondHandListings) {
if (p.getName().equals(l.seller)) mine++;
}
if (mine >= SH_MAX_PER_PLAYER) {
p.sendMessage("§c§l[二手商店] §f最多同时上架 §e" + SH_MAX_PER_PLAYER + " §f件商品");
return;
}
ItemStack hand = p.getInventory().getItemInMainHand();
if (hand == null || hand.getType() == Material.AIR) {
p.sendMessage("§c§l[二手商店] §f请先手持要上架的物品");
return;
}
// 暂存主手物品并清空
pendingPublishItem.put(p.getUniqueId(), hand.clone());
p.getInventory().setItemInMainHand(null);
publishListening.add(p.getUniqueId());
Bukkit.getScheduler().runTask(plugin, () -> {
if (p.isOnline()) p.closeInventory();
});
p.sendMessage("§6§l[二手商店] §e请在聊天栏输入售价(债券,整数,封底 §f0§e)");
p.sendMessage("§715秒内输入,超时自动退还物品");
Bukkit.getScheduler().runTaskLater(plugin, () -> {
if (publishListening.remove(p.getUniqueId())) {
ItemStack back = pendingPublishItem.remove(p.getUniqueId());
if (back != null) {
HashMap<Integer, ItemStack> left = p.getInventory().addItem(back);
for (ItemStack drop : left.values()) {
p.getWorld().dropItemNaturally(p.getLocation(), drop);
}
}
Player pl = Bukkit.getPlayer(p.getUniqueId());
if (pl != null && pl.isOnline()) {
pl.sendMessage("§c[二手商店] 上架超时,物品已退还");
}
}
}, 300L);
}
// ===== 聊天输入售价 =====
@EventHandler(priority = EventPriority.LOW)
public void onPublishChat(org.bukkit.event.player.AsyncPlayerChatEvent e) {
Player p = e.getPlayer();
UUID uuid = p.getUniqueId();
if (!publishListening.contains(uuid)) return;
e.setCancelled(true);
publishListening.remove(uuid);
ItemStack item = pendingPublishItem.remove(uuid);
if (item == null) return;
String msg = e.getMessage().trim();
if (msg.isEmpty() || msg.equals("取消")) {
returnItem(p, item);
p.sendMessage("§7[二手商店] 已取消上架,物品退还");
return;
}
int price;
try {
price = Integer.parseInt(msg);
} catch (Exception ex) {
returnItem(p, item);
p.sendMessage("§c[二手商店] 售价必须是整数,已取消上架并退还物品");
return;
}
if (price < 0) price = 0; // 封底 0 债券
SecondHandListing l = new SecondHandListing();
l.id = nextSecondHandId();
l.seller = p.getName();
l.price = price;
l.itemB64 = itemToBase64(item);
l.createdAt = System.currentTimeMillis();
if (l.itemB64 == null || l.itemB64.isEmpty()) {
returnItem(p, item);
p.sendMessage("§c[二手商店] 物品序列化失败,已退还");
return;
}
secondHandListings.add(l);
saveSecondHand();
p.sendMessage("§a§l[二手商店] §f上架成功: §e" + item.getType().name()
+ " §7x" + item.getAmount() + " §6" + price + " §7枚债券");
openSecondHandShop(p, 0);
}
private void returnItem(Player p, ItemStack item) {
if (item == null) return;
HashMap<Integer, ItemStack> left = p.getInventory().addItem(item);
for (ItemStack drop : left.values()) {
p.getWorld().dropItemNaturally(p.getLocation(), drop);
}
}
// ===== 购买二手商品 =====
private void buySecondHand(Player p, SecondHandListing l) {
if (l == null) return;
// 卖家自己不可购买
if (p.getName().equals(l.seller)) {
p.sendMessage("§c§l[二手商店] §f不能购买自己上架的商品");
return;
}
// 冻结拦截:买家
if (plugin.getBonds().isFrozen(p.getName())) {
p.sendMessage("§c§l[二手商店] §f债券账户被冻结,不可购买/出售商品");
return;
}
// 冻结拦截:卖家(出售方)也被冻结则不可成交
if (plugin.getBonds().isFrozen(l.seller)) {
p.sendMessage("§c§l[二手商店] §f卖家债券账户被冻结,该商品暂不可购买");
return;
}
// 扣买家钱(0 债券直接放行)
if (l.price > 0) {
boolean ok = plugin.getBonds().deductBonds(
p.getName(), l.price, "shop_secondhand",
l.seller, "二手商店",
"购买二手商品#" + l.id);
if (!ok) {
p.sendMessage("§c§l[二手商店] §f债券不足或扣款失败");
return;
}
}
// 给卖家钱
if (l.price > 0) {
plugin.getBonds().addBonds(
l.seller, l.price, "shop_secondhand",
p.getName(), "二手商店",
"出售二手商品#" + l.id);
}
// 发货
ItemStack is = base64ToItem(l.itemB64);
if (is == null) {
p.sendMessage("§c§l[二手商店] §f商品数据损坏,已退款");
if (l.price > 0) {
plugin.getBonds().addBonds(
p.getName(), l.price, "shop_secondhand_refund",
l.seller, "二手商店", "商品损坏退款#" + l.id);
}
secondHandListings.remove(l);
saveSecondHand();
return;
}
HashMap<Integer, ItemStack> left = p.getInventory().addItem(is);
for (ItemStack drop : left.values()) {
p.getWorld().dropItemNaturally(p.getLocation(), drop);
}
secondHandListings.remove(l);
saveSecondHand();
p.sendMessage("§a§l[二手商店] §f购买成功: §e" + is.getType().name()
+ " §7x" + is.getAmount() + " §6" + l.price + " §7枚债券");
Player seller = Bukkit.getPlayer(l.seller);
if (seller != null && seller.isOnline()) {
seller.sendMessage("§a§l[二手商店] §f你的商品已售出: §e" + is.getType().name()
+ " §7x" + is.getAmount() + " §6" + l.price + " §7枚债券");
}
openSecondHandShop(p, getSecondHandPage(p.getUniqueId()));
}
}
+3
View File
@@ -96,6 +96,9 @@ commands:
shop :
description: 游戏商人
usage: /shop
shopbuy :
description: 二手交易商店
usage: /shopbuy
商店:
description: 打开商店
usage: /商店