领地扩建按创建单价扣费、收缩按比例退款、圈地棒SN归属校验;发射器/投掷器消耗SN永久锁定不支持注销
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 2m9s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s

This commit is contained in:
youpaishidifu
2026-10-03 10:21:11 +08:00
parent e13869923f
commit 643f2a81cf
2 changed files with 173 additions and 4 deletions
+135 -4
View File
@@ -5405,6 +5405,22 @@ public class AreaProtection implements Listener {
|| action == Action.RIGHT_CLICK_BLOCK);
if (isLeft || isRight) {
e.setCancelled(true);
// ★ SN 归属校验(2026-10-03):只认本人的圈地工具,防止借他人工具圈地后消耗掉别人的申领资格
if (!isAreaAdmin(p)) {
SnManager snm = plugin.getSnManager();
String wandSn = (snm != null) ? snm.readSn(hand) : null;
Map<String, Object> wandRow = (wandSn != null) ? snm.getSn(wandSn) : null;
Object wandOwnerObj = (wandRow != null) ? wandRow.get("owner") : null;
String wandOwner = (wandOwnerObj == null) ? null : String.valueOf(wandOwnerObj);
if (wandSn == null || wandOwner == null || wandOwner.isEmpty()
|| !samePlayer(wandOwner, p.getName())) {
p.sendMessage("§c§l[防护] §f请使用你自己的圈地工具选区——"
+ (wandSn == null
? "该工具未绑定你本人,请用 §e/protect 工具 §f重新申领"
: "这是 §e" + wandOwner + " §f的圈地工具"));
return;
}
}
Block block = e.getClickedBlock();
if (block == null) {
try { block = p.getTargetBlockExact(5); }
@@ -8080,14 +8096,36 @@ public class AreaProtection implements Listener {
return true;
}
// ★ 扩建扣费(2026-10-03):与创建同单价,余额不足则拒绝扩建
int expPrice = landPricePerSqm(p);
long expCostL = deltaSq * (long) expPrice;
if (expCostL > Integer.MAX_VALUE) {
p.sendMessage("§c§l[防护] §f本次扩建费用超出上限,请分次扩建");
return true;
}
int expCost = (int) expCostL;
if (expCost > 0) {
BondManager ebm = plugin.getBonds();
if (ebm == null || !ebm.deductBonds(p.getName(), expCost, "land_expand",
p.getName(), p.getName(),
"扩建领地: " + ac.name + " (+" + deltaSq + "㎡×" + expPrice + ")")) {
p.sendMessage("§c§l[防护] §f余额不足,扩建需 §e" + expCost
+ " §f债券(" + deltaSq + "㎡×" + expPrice + "/㎡),请先充值再扩建");
return true;
}
}
ac.x1 = minX;
ac.x2 = maxX;
ac.z1 = minZ;
ac.z2 = maxZ;
ac.createCost += expCost; // ★ 计入删除退费基数(后续删除按比例退)
saveAreaToDb(ac);
p.sendMessage("§a§l[防护] §f区域 §e" + ac.name
+ " §f向 §e" + dir + " §f扩建 §e"
+ amount + "§f格");
+ amount + "§f格"
+ (expCost > 0
? (",扣除 §e" + expCost + " §f债券(" + deltaSq + "㎡×" + expPrice + "/㎡)")
: ""));
return true;
}
@@ -8125,6 +8163,18 @@ public class AreaProtection implements Listener {
int maxZ = Math.max(ac.z1, ac.z2);
float yaw = p.getLocation().getYaw();
String dir = yawToDir(yaw);
// ★ 收缩前置校验(2026-10-03):负数会变成"负数扩建"白嫖,一并挡掉
if (amount <= 0) {
p.sendMessage("§c§l[防护] §f收缩格数必须为正整数");
return true;
}
if (amount > 100000) {
p.sendMessage("§c§l[防护] §f单次收缩格数上限 100000,请分次收缩");
return true;
}
long oldSqW = (long) maxX - minX + 1;
long oldSqL = (long) maxZ - minZ + 1;
long oldSq = oldSqW * oldSqL;
if (dir.equals("北")) minZ += amount;
else if (dir.equals("南")) maxZ -= amount;
else if (dir.equals("东")) maxX -= amount;
@@ -8133,6 +8183,40 @@ public class AreaProtection implements Listener {
p.sendMessage("§c收缩过度");
return true;
}
long newSqW = (long) maxX - minX + 1;
long newSqL = (long) maxZ - minZ + 1;
long shrinkSq = oldSq - newSqW * newSqL;
// ★ 收缩退款(2026-10-03):单价 × 收缩面积 × 删除退款比例
// 基数不超过已付 createCost(历史免费地不凭空造钱),退款给领地主
int shrinkPrice = landPricePerSqm(p);
int shrinkRatio = globalRefundRatioPerCent;
if (shrinkRatio > 100) shrinkRatio = 100;
long baseL = shrinkSq * (long) shrinkPrice;
if (baseL > ac.createCost) baseL = ac.createCost;
if (baseL < 0) baseL = 0;
int refundBase = (int) baseL;
int shrinkRefund = 0;
if (shrinkRatio > 0 && refundBase > 0) {
shrinkRefund = (int) Math.floor(refundBase * (shrinkRatio / 100.0));
}
if (shrinkRefund > 0) {
BondManager sbm = plugin.getBonds();
if (sbm == null) {
shrinkRefund = 0;
} else {
int afterBal = sbm.addBonds(ac.owner, shrinkRefund, "land_refund",
ac.owner, p.getName(),
"收缩领地退费: " + ac.name + " (-" + shrinkSq + "㎡)");
if (afterBal < 0) {
plugin.getLogger().warning("[防护] 领地收缩退费失败(账户冻结?): "
+ ac.owner + " +" + shrinkRefund);
shrinkRefund = 0;
}
}
}
ac.createCost -= refundBase; // ★ 收缩掉的部分不再计入删除退费基数
if (ac.createCost < 0) ac.createCost = 0;
ac.x1 = minX;
ac.x2 = maxX;
ac.z1 = minZ;
@@ -8140,7 +8224,11 @@ public class AreaProtection implements Listener {
saveAreaToDb(ac);
p.sendMessage("§a§l[防护] §f区域 §e" + ac.name
+ " §f向 §e" + dir + " §f收缩 §e"
+ amount + "§f格");
+ amount + "§f格"
+ (shrinkRefund > 0
? (",退还 §e" + shrinkRefund + " §f债券("
+ shrinkSq + "㎡×" + shrinkPrice + "/㎡×" + shrinkRatio + "%)")
: (shrinkRatio <= 0 ? "§7(退款比例 0,不退费)" : "")));
return true;
}
@@ -9447,6 +9535,17 @@ public class AreaProtection implements Listener {
plugin.getLogger().info("[防护] 已上报 " + sent + " 名在线管理员: " + message.replaceAll("§.", ""));
}
/**
* ★ 扩建 / 收缩单价:与创建同一口径(用户组可覆盖,缺省为创建单价 globalCreatePricePerSqm)。
* 扩建收费、收缩退费都按这个单价算,保证"创建一次扣费、扩建不再白嫖"。
*/
private int landPricePerSqm(Player p) {
UserGroupManager ug = plugin.getUserGroup();
return (ug != null)
? ug.getPlayerLandPricePerSqm(p.getName(), globalCreatePricePerSqm)
: globalCreatePricePerSqm;
}
// ==================== 领地面积审批流(2026-09-29) ====================
/** 待审批的面积超限请求(expand=扩建提案 / create=创建提案) */
@@ -9540,6 +9639,10 @@ public class AreaProtection implements Listener {
+ newW + "×" + newL + "§f,边长上限 §e" + globalMaxLandSideBlocks
+ "§f),已提交管理员现场审批。");
p.sendMessage("§7管理员需进入该领地后执行 §a/protect accept " + ac.name + " §7或 §c/protect deny " + ac.name);
long peCostL = deltaSq * (long) landPricePerSqm(p);
p.sendMessage("§7审批通过时按创建单价 §e" + landPricePerSqm(p)
+ "/㎡ §f收取 §e" + (peCostL > Integer.MAX_VALUE ? "(超额,请分次扩建)" : String.valueOf(peCostL))
+ " §f债券");
reportLandRemovalToAdmins("§6§l[领地审批] §f玩家 §e" + p.getName()
+ " §f申请扩建领地 §e" + ac.name + " §7(+" + deltaSq + "㎡, 向" + dir + amount + "格, 新尺寸 "
+ newW + "×" + newL + ") §f——管理员到现场后 §a/protect accept " + ac.name + "§f/§cdeny");
@@ -9672,16 +9775,44 @@ public class AreaProtection implements Listener {
}
return;
}
// ★ 扩建审批扣费(2026-10-03):审批时按创建单价 × 新增面积收款
UserGroupManager ePugm = plugin.getUserGroup();
int ePrice = (ePugm != null)
? ePugm.getPlayerLandPricePerSqm(pe.requester, globalCreatePricePerSqm)
: globalCreatePricePerSqm;
long eCostL = pe.deltaSq * (long) ePrice;
if (eCostL > Integer.MAX_VALUE) {
adm.sendMessage("§c§l[审批] §f该扩建费用超出上限,无法扣费,申请保留待处理");
return;
}
int eCost = (int) eCostL;
int eCharged = 0;
if (eCost > 0) {
BondManager ebm = plugin.getBonds();
if (ebm == null || !ebm.deductBonds(pe.requester, eCost, "land_expand",
pe.requester, pe.requester,
"管理员审批扩建领地: " + pe.landName + " (+" + pe.deltaSq + "㎡×" + ePrice + ")")) {
adm.sendMessage("§c§l[审批] §f申请人 §e" + pe.requester
+ " §f余额不足(需 §e" + eCost + "§f),无法扣费,申请保留待处理");
if (requester != null && requester.isOnline()) {
requester.sendMessage("§c§l[防护] §f你的扩建申请因余额不足(需 " + eCost
+ " 债券)未能完成,请充值后请管理员重新审批");
}
return;
}
eCharged = eCost;
}
ac.x1 = pe.newX1; ac.x2 = pe.newX2;
ac.z1 = pe.newZ1; ac.z2 = pe.newZ2;
ac.createCost += eCharged; // ★ 计入删除退费基数
saveAreaToDb(ac);
pendingExpands.remove(pe.landName.toLowerCase());
adm.sendMessage("§a§l[审批] §f已批准 §e" + pe.landName + " §f扩建:+§e"
+ pe.deltaSq + "§f㎡(" + pe.oldW + "×" + pe.oldL + " → §e"
+ pe.newW + "×" + pe.newL + "§f)");
+ pe.newW + "×" + pe.newL + "§f),向申请人扣除 §e" + eCharged + " §f债券");
if (requester != null && requester.isOnline()) {
requester.sendMessage("§a§l[防护] §f你的领地 §e" + pe.landName
+ " §f扩建申请已被管理员批准,已生效");
+ " §f扩建申请已被管理员批准,已生效,扣除 " + eCharged + " 债券");
}
plugin.getLogger().info("[领地审批] 扩建通过: " + pe.landName + " (申请人 " + pe.requester
+ ", 审批人 " + adm.getName() + ") +" + pe.deltaSq + "㎡");
+38
View File
@@ -17,6 +17,7 @@ import org.bukkit.event.entity.EntityCombustEvent;
import org.bukkit.event.entity.EntityPickupItemEvent;
import org.bukkit.event.entity.ItemDespawnEvent;
import org.bukkit.event.inventory.CraftItemEvent;
import org.bukkit.event.block.BlockDispenseEvent;
import org.bukkit.event.inventory.FurnaceBurnEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryOpenEvent;
@@ -1330,6 +1331,31 @@ public class SnManager implements Listener {
}
}
/**
* 发射器 / 投掷器把 SN 物品发射出去(2026-10-03):
* 雪球菜单等管控物品一经机器发射即视为"已使用 / 分解"——永久锁定该 SN:
* <ul>
* <li>不再发放:findActive 把 illegal 计入有效登记,同种类无法再申领;</li>
* <li>不支持注销 / 补发:doCancel、doReissue 对 illegal 直接拒绝。</li>
* </ul>
* BlockDispenseEvent 在发射器与投掷器上都会触发(投掷器走同一条 dispense 逻辑)。
*/
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onDispense(BlockDispenseEvent e) {
try {
ItemStack out = e.getItem();
if (out == null) return;
String type = detectType(out);
if (type == null || !WATCH_TYPES.contains(type)) return;
String sn = readSn(out);
if (sn == null) return;
bindIllegal(sn, "发射器/投掷器发射消耗");
enqueue("illegal_dispense", sn, "", "发射器发射");
} catch (Throwable t) {
throttleErr("dispense:" + t.getMessage());
}
}
private void scanMatrix(ItemStack[] matrix, Player p, String why) {
if (matrix == null) return;
for (ItemStack it : matrix) {
@@ -2273,6 +2299,12 @@ public class SnManager implements Listener {
Map<String, Object> row = getSn(sn);
if (row == null) return "SN不存在";
String owner = str(row.get("owner"));
// ★ 永久锁定的 SN(发射器发射消耗等):不支持注销(2026-10-03)
if (ST_ILLEGAL.equals(str(row.get("status")))) {
logSn(sn, str(row.get("item_type")), "cancel_deny", player,
"SN已永久锁定(视为已使用/分解),不支持注销");
return "该SN已被永久锁定(视为已使用/分解),不支持注销";
}
// ★ 统一门槛:不在本人身上 ∩ 不在本人领地箱子 ∩ 脱离管控 > 12 小时。
// 管理员「强制注销」(force) 按定义豁免三项;「代办注销」照常校验。
// 未达标对玩家只给通用回执,明细留在管理端。
@@ -2308,6 +2340,12 @@ public class SnManager implements Listener {
Map<String, Object> row = getSn(oldSn);
if (row == null) return "SN不存在";
String owner = str(row.get("owner"));
// ★ 永久锁定的 SN(发射器发射消耗等):不支持补发(2026-10-03)
if (ST_ILLEGAL.equals(str(row.get("status")))) {
logSn(oldSn, itemType, "reissue_deny", player,
"SN已永久锁定(视为已使用/分解),不支持补发");
return "该SN已被永久锁定(视为已使用/分解),不支持补发";
}
if (player != null && !player.isEmpty()) owner = player;
if (itemType == null || itemType.isEmpty())
itemType = str(row.get("item_type"));