Compare commits
30
Commits
v2.92-test
...
v2.91
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2742d8a6e | ||
|
|
19eebf1358 | ||
|
|
78d3e11c21 | ||
|
|
002b0e47a9 | ||
|
|
adae149791 | ||
|
|
668a5f3d38 | ||
|
|
27ac366046 | ||
|
|
dea31b8222 | ||
|
|
233060e7bd | ||
|
|
8388d68cb7 | ||
|
|
2bcc449bc4 | ||
|
|
d8ff4b0c92 | ||
|
|
c65b64030f | ||
|
|
448f23ac53 | ||
|
|
fb8b06c4e5 | ||
|
|
4ab2b2dfd8 | ||
|
|
baba10b84d | ||
|
|
5e98830829 | ||
|
|
8e84c58bcc | ||
|
|
b6ad95171d | ||
|
|
c1bad334b7 | ||
|
|
1e5576432d | ||
|
|
fbdcca4089 | ||
|
|
fd2728fe42 | ||
|
|
385301432a | ||
|
|
989b86f6e6 | ||
|
|
e848746800 | ||
|
|
915d7d1143 | ||
|
|
e00a2f6e4b | ||
|
|
720757463b |
@@ -52,45 +52,72 @@ jobs:
|
||||
ls -la target/*.jar
|
||||
echo "Verification completed"
|
||||
|
||||
- name: Generate Release Notes
|
||||
run: |
|
||||
echo "Generating release notes..."
|
||||
VERSION=$(grep "^version:" src/main/resources/plugin.yml | awk '{print $2}' | tr -d '\r\n ')
|
||||
TAG="v$VERSION"
|
||||
|
||||
# Generate release notes from last 10 commits
|
||||
echo "## Changes in $TAG" > /tmp/release_body.md
|
||||
echo "" >> /tmp/release_body.md
|
||||
git log --format="- **%h** %s (%an, %ad)" --date=short -10 >> /tmp/release_body.md
|
||||
echo "" >> /tmp/release_body.md
|
||||
echo "### Full changelog" >> /tmp/release_body.md
|
||||
echo "" >> /tmp/release_body.md
|
||||
echo "[View all commits](/youpaishidifu/Sdf1_login/commits/main)" >> /tmp/release_body.md
|
||||
|
||||
echo "Release notes generated for $TAG"
|
||||
cat /tmp/release_body.md
|
||||
|
||||
- name: Create Release
|
||||
env:
|
||||
RUNNER_TOKEN: ${ACTIONS_RUNTIME_TOKEN}
|
||||
GITEA_TOKEN: 194187cbdc8e1f1d3bc810b1b9e07ddc904a1f73
|
||||
run: |
|
||||
set -e
|
||||
VERSION=$(grep "^version:" src/main/resources/plugin.yml | awk '{print $2}' | tr -d '\r\n ')
|
||||
TAG="v${VERSION}"
|
||||
BASE="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}"
|
||||
echo "Version: ${VERSION}, Tag: ${TAG}"
|
||||
|
||||
echo "=== Checking existing tag ${TAG} ==="
|
||||
RESP=$(curl -s -w "\nHTTP_CODE:%{http_code}" -H "Authorization: token ${RUNNER_TOKEN}" "${BASE}/releases/tags/${TAG}")
|
||||
HTTP_CODE=$(echo "$RESP" | grep -o "HTTP_CODE:[0-9]*" | cut -d: -f2)
|
||||
echo "Check tag HTTP: ${HTTP_CODE}"
|
||||
|
||||
TAG="v$VERSION"
|
||||
BASE="$GITHUB_API_URL/repos/$GITHUB_REPOSITORY"
|
||||
echo "Version: $VERSION, Tag: $TAG"
|
||||
|
||||
echo "=== Checking existing tag $TAG ==="
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $GITEA_TOKEN" "$BASE/releases/tags/$TAG")
|
||||
echo "Check tag HTTP: $HTTP_CODE"
|
||||
|
||||
if [ "$HTTP_CODE" = "200" ]; then
|
||||
OLD_ID=$(echo "$RESP" | head -1 | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null)
|
||||
echo "Tag ${TAG} exists (release #${OLD_ID}), deleting..."
|
||||
curl -s -X DELETE -H "Authorization: token ${RUNNER_TOKEN}" "${BASE}/releases/${OLD_ID}"
|
||||
curl -s -X DELETE -H "Authorization: token ${RUNNER_TOKEN}" "${BASE}/tags/${TAG}"
|
||||
OLD_ID=$(curl -s -H "Authorization: token $GITEA_TOKEN" "$BASE/releases/tags/$TAG" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))")
|
||||
echo "Tag $TAG exists (release #$OLD_ID), deleting..."
|
||||
curl -s -X DELETE -H "Authorization: token $GITEA_TOKEN" "$BASE/releases/$OLD_ID"
|
||||
curl -s -X DELETE -H "Authorization: token $GITEA_TOKEN" "$BASE/tags/$TAG"
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
|
||||
echo "=== Creating release ==="
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token ${RUNNER_TOKEN}" \
|
||||
# Use python3 to construct proper JSON
|
||||
python3 -c "
|
||||
import json, sys
|
||||
body = open('/tmp/release_body.md').read()
|
||||
data = {'tag_name': '$TAG', 'name': '$TAG', 'body': body, 'draft': False, 'prerelease': False}
|
||||
print(json.dumps(data))
|
||||
" > /tmp/release_payload.json
|
||||
|
||||
RESP=$(curl -s -X POST \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${BASE}/releases" \
|
||||
-d "{"tag_name":"${TAG}","name":"${TAG}","body":"Auto release by Gitea Actions","draft":false,"prerelease":false}"
|
||||
|
||||
"$BASE/releases" \
|
||||
-d @/tmp/release_payload.json)
|
||||
|
||||
RELEASE_ID=$(echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))")
|
||||
echo "Created release #$RELEASE_ID"
|
||||
|
||||
echo "=== Uploading artifact ==="
|
||||
JAR=$(ls target/*.jar | head -1)
|
||||
UPLOAD_RESP=$(curl -s -w "\nHTTP_CODE:%{http_code}" -X POST \
|
||||
-H "Authorization: token ${RUNNER_TOKEN}" \
|
||||
UPLOAD_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/jar" \
|
||||
"${BASE}/releases/tags/${TAG}/assets?name=$(basename ${JAR})" \
|
||||
--data-binary "@${JAR}")
|
||||
UPLOAD_CODE=$(echo "$UPLOAD_RESP" | grep -o "HTTP_CODE:[0-9]*" | cut -d: -f2)
|
||||
echo "Upload HTTP: ${UPLOAD_CODE}"
|
||||
echo "$UPLOAD_RESP" | head -1
|
||||
|
||||
echo "=== Release ${TAG} created successfully ==="
|
||||
"$BASE/releases/$RELEASE_ID/assets?name=$(basename $JAR)" \
|
||||
--data-binary "@$JAR")
|
||||
echo "Upload HTTP: $UPLOAD_CODE"
|
||||
|
||||
echo "=== Release $TAG created successfully ==="
|
||||
|
||||
|
||||
@@ -106,3 +106,11 @@ backup/
|
||||
*.rar
|
||||
test.*
|
||||
/*.rev
|
||||
# 屏蔽全部日志文件
|
||||
*.log
|
||||
# 屏蔽开发计划相关文件
|
||||
*.xlsx
|
||||
*.docs
|
||||
*.doc
|
||||
*.txt
|
||||
|
||||
|
||||
@@ -143,6 +143,7 @@ public class AreaCLIManager {
|
||||
String maxLands = areaProtect.getAreaConfigValue("max_lands_per_player");
|
||||
String height = areaProtect.getAreaConfigValue("default_height");
|
||||
String peace = areaProtect.getAreaConfigValue("peace_mode_max_duration");
|
||||
String refund = areaProtect.getAreaConfigValue("refund_ratio_per_cent");
|
||||
|
||||
// 创建价格
|
||||
p.sendMessage(Component.empty()
|
||||
@@ -188,6 +189,19 @@ public class AreaCLIManager {
|
||||
.hoverEvent(HoverEvent.showText(Component.text("§e点击后输入新值"))))
|
||||
);
|
||||
|
||||
// 退费比例
|
||||
p.sendMessage(Component.empty()
|
||||
.append(Component.text("§a删除退费比例(%): §f" + (refund != null ? refund : "0")
|
||||
+ (((refund == null ? 0 : Integer.parseInt(refund)) <= 0) ? " §7(已关闭退费)"
|
||||
: ((refund != null && Integer.parseInt(refund) >= 100) ? " §7(全额退款)" : " §7(原价×比例)")) + " "))
|
||||
.append(Component.text("§a[+10] ")
|
||||
.clickEvent(ClickEvent.suggestCommand("/protect config refund_ratio " + safeParseInt(refund, 0, 10)))
|
||||
.hoverEvent(HoverEvent.showText(Component.text("§e点击后输入新值(自动限制0~100)"))))
|
||||
.append(Component.text("§c[-10] ")
|
||||
.clickEvent(ClickEvent.suggestCommand("/protect config refund_ratio " + safeParseInt(refund, 0, -10)))
|
||||
.hoverEvent(HoverEvent.showText(Component.text("§e点击后输入新值(自动限制0~100)"))))
|
||||
);
|
||||
|
||||
p.sendMessage(Component.text("§7提示: 点击修改值后,可在聊天栏输入任意数字进行精确调整"));
|
||||
p.sendMessage(Component.text("§7支持: 纯数字、中文数字、中文大写、罗马数字、英文数字"));
|
||||
|
||||
@@ -540,6 +554,8 @@ public class AreaCLIManager {
|
||||
case "move": oldState = land.denyMove; land.denyMove = !land.denyMove; break;
|
||||
case "block_place": oldState = land.denyBlockPlace; land.denyBlockPlace = !land.denyBlockPlace; break;
|
||||
case "block_break": oldState = land.denyBlockBreak; land.denyBlockBreak = !land.denyBlockBreak; break;
|
||||
case "spawn_egg": oldState = land.denySpawnEgg; land.denySpawnEgg = !land.denySpawnEgg; break;
|
||||
case "wax": oldState = land.denyWax; land.denyWax = !land.denyWax; break;
|
||||
case "entity_interact": oldState = land.denyEntityInteract; land.denyEntityInteract = !land.denyEntityInteract; break;
|
||||
case "container": oldState = land.denyContainer; land.denyContainer = !land.denyContainer; break;
|
||||
case "pvp": oldState = land.denyPVP; land.denyPVP = !land.denyPVP; break;
|
||||
@@ -600,6 +616,8 @@ public class AreaCLIManager {
|
||||
case "move": return "denyMove";
|
||||
case "block_place": return "denyBlockPlace";
|
||||
case "block_break": return "denyBlockBreak";
|
||||
case "spawn_egg": return "denySpawnEgg";
|
||||
case "wax": return "denyWax";
|
||||
case "entity_interact": return "denyEntityInteract";
|
||||
case "container": return "denyContainer";
|
||||
case "pvp": return "denyPVP";
|
||||
@@ -661,6 +679,8 @@ public class AreaCLIManager {
|
||||
perms.add(new PermItem("move", "移动", !land.denyMove));
|
||||
perms.add(new PermItem("block_place", "放置方块", !land.denyBlockPlace));
|
||||
perms.add(new PermItem("block_break", "破坏方块", !land.denyBlockBreak));
|
||||
perms.add(new PermItem("spawn_egg", "生物蛋", !land.denySpawnEgg));
|
||||
perms.add(new PermItem("wax", "涂蜡/刮蜡", !land.denyWax));
|
||||
perms.add(new PermItem("entity_interact", "实体交互(船/矿车/盔甲架/展示框)", !land.denyEntityInteract));
|
||||
perms.add(new PermItem("container", "容器管理", !land.denyContainer));
|
||||
perms.add(new PermItem("pvp", "玩家对战", !land.denyPVP));
|
||||
@@ -703,6 +723,8 @@ public class AreaCLIManager {
|
||||
case "move": return "移动";
|
||||
case "block_place": return "放置方块";
|
||||
case "block_break": return "破坏方块";
|
||||
case "spawn_egg": return "生物蛋";
|
||||
case "wax": return "涂蜡/刮蜡";
|
||||
case "container": return "容器管理";
|
||||
case "pvp": return "玩家对战";
|
||||
case "mount": return "骑乘坐具";
|
||||
@@ -1120,6 +1142,8 @@ public class AreaCLIManager {
|
||||
{"move", "移动", "denyMove"},
|
||||
{"block_place", "放置方块", "denyBlockPlace"},
|
||||
{"block_break", "破坏方块", "denyBlockBreak"},
|
||||
{"spawn_egg", "生物蛋", "denySpawnEgg"},
|
||||
{"wax", "涂蜡/刮蜡", "denyWax"},
|
||||
{"entity_interact", "实体交互", "denyEntityInteract"},
|
||||
{"container", "容器管理", "denyContainer"},
|
||||
{"pvp", "玩家对战", "denyPVP"},
|
||||
@@ -1215,6 +1239,8 @@ public class AreaCLIManager {
|
||||
case "denyMove": return land.denyMove;
|
||||
case "denyBlockPlace": return land.denyBlockPlace;
|
||||
case "denyBlockBreak": return land.denyBlockBreak;
|
||||
case "denySpawnEgg": return land.denySpawnEgg;
|
||||
case "denyWax": return land.denyWax;
|
||||
case "denyEntityInteract": return land.denyEntityInteract;
|
||||
case "denyContainer": return land.denyContainer;
|
||||
case "denyPVP": return land.denyPVP;
|
||||
|
||||
@@ -487,6 +487,7 @@ public class AreaGUIManager implements Listener {
|
||||
// 权限列表
|
||||
String[][] permDefs = {
|
||||
{"移动", "denyMove"}, {"放置方块", "denyBlockPlace"}, {"破坏方块", "denyBlockBreak"},
|
||||
{"生物蛋", "denySpawnEgg"}, {"涂蜡/刮蜡", "denyWax"},
|
||||
{"实体交互", "denyEntityInteract"},
|
||||
{"容器管理", "denyContainer"}, {"玩家对战", "denyPVP"}, {"骑乘坐具", "denyMount"}, {"投掷末影珍珠", "denyEnderPearl"},
|
||||
{"投掷物", "denyThrownProjectiles"}, {"禁止袭击", "denyRaid"}, {"弓箭射击", "denyBow"},
|
||||
@@ -585,6 +586,11 @@ public class AreaGUIManager implements Listener {
|
||||
case "denyMove": return land.denyMove;
|
||||
case "denyBlockPlace": return land.denyBlockPlace;
|
||||
case "denyBlockBreak": return land.denyBlockBreak;
|
||||
case "denySpawnEgg": return land.denySpawnEgg;
|
||||
case "denyWax": return land.denyWax;
|
||||
case "denyMobAttack": return land.denyMobAttack;
|
||||
case "denyFarmlandTrample": return land.denyFarmlandTrample;
|
||||
case "denyEnderTeleport": return land.denyEnderTeleport;
|
||||
case "denyEntityInteract": return land.denyEntityInteract;
|
||||
case "denyContainer": return land.denyContainer;
|
||||
case "denyPVP": return land.denyPVP;
|
||||
@@ -669,6 +675,8 @@ public class AreaGUIManager implements Listener {
|
||||
perms.add(new PermEntry("移动", !land.denyMove));
|
||||
perms.add(new PermEntry("放置方块", !land.denyBlockPlace));
|
||||
perms.add(new PermEntry("破坏方块", !land.denyBlockBreak));
|
||||
perms.add(new PermEntry("生物蛋", !land.denySpawnEgg));
|
||||
perms.add(new PermEntry("涂蜡/刮蜡", !land.denyWax));
|
||||
perms.add(new PermEntry("实体交互(船/矿车/盔甲架/展示框)", !land.denyEntityInteract));
|
||||
perms.add(new PermEntry("容器管理", !land.denyContainer));
|
||||
perms.add(new PermEntry("玩家对战", !land.denyPVP));
|
||||
@@ -727,6 +735,8 @@ public class AreaGUIManager implements Listener {
|
||||
case "移动": land.denyMove = !land.denyMove; break;
|
||||
case "放置方块": land.denyBlockPlace = !land.denyBlockPlace; break;
|
||||
case "破坏方块": land.denyBlockBreak = !land.denyBlockBreak; break;
|
||||
case "生物蛋": land.denySpawnEgg = !land.denySpawnEgg; break;
|
||||
case "涂蜡/刮蜡": land.denyWax = !land.denyWax; break;
|
||||
case "实体交互(船/矿车/盔甲架/展示框)": land.denyEntityInteract = !land.denyEntityInteract; break;
|
||||
case "容器管理": land.denyContainer = !land.denyContainer; break;
|
||||
case "玩家对战": land.denyPVP = !land.denyPVP; break;
|
||||
@@ -1153,6 +1163,7 @@ public class AreaGUIManager implements Listener {
|
||||
// 切换权限
|
||||
String[][] permDefs = {
|
||||
{"移动", "denyMove"}, {"放置方块", "denyBlockPlace"}, {"破坏方块", "denyBlockBreak"},
|
||||
{"生物蛋", "denySpawnEgg"}, {"涂蜡/刮蜡", "denyWax"},
|
||||
{"实体交互", "denyEntityInteract"},
|
||||
{"容器管理", "denyContainer"}, {"玩家对战", "denyPVP"}, {"骑乘坐具", "denyMount"}, {"投掷末影珍珠", "denyEnderPearl"},
|
||||
{"投掷物", "denyThrownProjectiles"}, {"禁止袭击", "denyRaid"}, {"弓箭射击", "denyBow"},
|
||||
@@ -1164,7 +1175,8 @@ public class AreaGUIManager implements Listener {
|
||||
{"农作物收获", "denyCropHarvest"}, {"剪切羊毛", "denyWoolShear"}, {"投喂动物", "denyAnimalFeeding"},
|
||||
{"攻击生物", "denyMobAttack"},
|
||||
{"禁止编辑告示牌", "denySignEdit"},
|
||||
{"玩家发光", "denyGlowing"}, {"传送", "allowTeleport"}, {"和平模式", "peaceMode"}
|
||||
{"玩家发光", "denyGlowing"}, {"传送", "allowTeleport"}, {"和平模式", "peaceMode"},
|
||||
{"耕地破坏", "denyFarmlandTrample"}, {"末影人行为", "denyEnderTeleport"}
|
||||
};
|
||||
|
||||
if (raw < permDefs.length) {
|
||||
@@ -1655,6 +1667,7 @@ public class AreaGUIManager implements Listener {
|
||||
int maxLands = 5;
|
||||
int defaultHeight = 255;
|
||||
int peaceDuration = 3600;
|
||||
int refundRatio = 0;
|
||||
try {
|
||||
String val = areaProtect.getAreaConfigValue("create_price_per_sqm");
|
||||
if (val != null) pricePerSqm = Integer.parseInt(val);
|
||||
@@ -1664,6 +1677,8 @@ public class AreaGUIManager implements Listener {
|
||||
if (val != null) defaultHeight = Integer.parseInt(val);
|
||||
val = areaProtect.getAreaConfigValue("peace_mode_max_duration");
|
||||
if (val != null) peaceDuration = Integer.parseInt(val);
|
||||
val = areaProtect.getAreaConfigValue("refund_ratio_per_cent");
|
||||
if (val != null) refundRatio = Integer.parseInt(val);
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
// 每平米价格(位置11)
|
||||
@@ -1684,6 +1699,16 @@ public class AreaGUIManager implements Listener {
|
||||
"",
|
||||
"§e左键+32 / 右键-32"));
|
||||
|
||||
// 删除退费比例(位置16)
|
||||
inv.setItem(16, createItem(Material.GOLD_INGOT, "§e§l删除领地退费比例",
|
||||
"§7当前: §f" + refundRatio + "% " + (refundRatio <= 0 ? "§c(已关闭退费)"
|
||||
: (refundRatio >= 100 ? "§a(全额退款)" : "§e(原价×比例)")),
|
||||
"",
|
||||
"§70=不退款 100=全额退款 其他=原价×比例%",
|
||||
"§7强制删除(-f)与自动清理不退费",
|
||||
"",
|
||||
"§e左键+10 / 右键-10(自动限制0~100)"));
|
||||
|
||||
// 和平模式最大时间(位置22)
|
||||
inv.setItem(22, createItem(Material.SHIELD, "§e§l和平模式最长时间",
|
||||
"§7当前: §f" + peaceDuration + " 秒",
|
||||
@@ -1768,6 +1793,11 @@ public class AreaGUIManager implements Listener {
|
||||
p.closeInventory();
|
||||
p.sendMessage("§e§l[配置] §f请输入新的默认高度:");
|
||||
areaProtect.setPendingConfigInput(p.getUniqueId(), "default_height");
|
||||
} else if (raw == 16) {
|
||||
// 删除退费比例
|
||||
p.closeInventory();
|
||||
p.sendMessage("§e§l[配置] §f请输入删除领地的退费比例(%),0=关闭,100=全额退款:");
|
||||
areaProtect.setPendingConfigInput(p.getUniqueId(), "refund_ratio_per_cent");
|
||||
} else if (raw == 22) {
|
||||
// 和平模式时间
|
||||
p.closeInventory();
|
||||
|
||||
@@ -117,6 +117,7 @@ public class AreaProtection implements Listener {
|
||||
private int globalMaxLandsPerPlayer = 5; // 每人最多领地数
|
||||
private int globalMaxHomePerPlayer = 5; // 每人最多传送点(home)数量
|
||||
private int globalDefaultHeight = 255; // 默认高度
|
||||
private int globalRefundRatioPerCent = 0; // 删除领地退费比例(0=关闭退费,100=全额退款,其他=原价×比例%)
|
||||
|
||||
// 选地
|
||||
private static final Material WAND = Material.BLAZE_ROD;
|
||||
@@ -427,6 +428,8 @@ public class AreaProtection implements Listener {
|
||||
loadAllAreas();
|
||||
loadWhitelists();
|
||||
recoverPendingEffects();
|
||||
// ★ 封禁/注销领主的领地自动清理(每5分钟扫描)
|
||||
startBannedOwnerSweepTask();
|
||||
}
|
||||
public void recoverPendingEffects() {
|
||||
try {
|
||||
@@ -1230,6 +1233,8 @@ public class AreaProtection implements Listener {
|
||||
try { ac.denyContainer = rs.getInt("deny_container") == 1; } catch (Exception ignored) {}
|
||||
try { ac.denyMobAttack = rs.getInt("deny_mob_attack") == 1; } catch (Exception ignored) {}
|
||||
try { ac.denySignEdit = rs.getInt("deny_sign_edit") == 1; } catch (Exception ignored) {}
|
||||
try { ac.denySpawnEgg = rs.getInt("deny_spawn_egg") == 1; } catch (Exception ignored) {}
|
||||
try { ac.denyWax = rs.getInt("deny_wax") == 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) {}
|
||||
@@ -1250,6 +1255,7 @@ public class AreaProtection implements Listener {
|
||||
ac.enableAnnounce = rs.getInt("enable_announce") == 1;
|
||||
ac.announceTemplate = rs.getString("announce_template");
|
||||
ac.txtContent = rs.getString("txt_content");
|
||||
try { ac.createCost = rs.getInt("create_cost"); } catch (Exception ignored) {}
|
||||
areas.put(ac.name, ac);
|
||||
count++;
|
||||
}
|
||||
@@ -1524,6 +1530,11 @@ public class AreaProtection implements Listener {
|
||||
try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_fluid INTEGER DEFAULT 0"); } catch (Exception ignored) {}
|
||||
// ★ 二级权限:实体交互(受一级放置/破坏总开关影响)
|
||||
try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_entity_interact INTEGER DEFAULT 0"); } catch (Exception ignored) {}
|
||||
try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN create_cost INTEGER DEFAULT 0"); } catch (Exception ignored) {}
|
||||
// ★ 生物蛋权限列(上级权限:放置方块)
|
||||
try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_spawn_egg INTEGER DEFAULT 0"); } catch (Exception ignored) {}
|
||||
// ★ 涂蜡/刮蜡权限列(上级权限:破坏方块/放置方块)
|
||||
try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_wax INTEGER DEFAULT 0"); } catch (Exception ignored) {}
|
||||
|
||||
// ★ 全局配置默认值
|
||||
try {
|
||||
@@ -1538,6 +1549,8 @@ public class AreaProtection implements Listener {
|
||||
stmt.executeUpdate("INSERT OR IGNORE INTO area_config VALUES ('default_height', '255')");
|
||||
stmt.executeUpdate("INSERT OR IGNORE INTO area_config VALUES ('peace_mode_max_duration', '3600')");
|
||||
}
|
||||
// ★ 退费比例配置(新键,已存在则忽略)
|
||||
try { stmt.executeUpdate("INSERT OR IGNORE INTO area_config VALUES ('refund_ratio_per_cent', '0')"); } catch (Exception ignored) {}
|
||||
// 读取全局配置
|
||||
ResultSet cfgRs = stmt.executeQuery("SELECT key, value FROM area_config");
|
||||
while (cfgRs.next()) {
|
||||
@@ -1549,6 +1562,7 @@ public class AreaProtection implements Listener {
|
||||
case "max_lands_per_player": globalMaxLandsPerPlayer = Integer.parseInt(v); break;
|
||||
case "max_home_per_player": globalMaxHomePerPlayer = Integer.parseInt(v); break;
|
||||
case "default_height": globalDefaultHeight = Integer.parseInt(v); break;
|
||||
case "refund_ratio_per_cent": globalRefundRatioPerCent = Integer.parseInt(v); break;
|
||||
}
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
@@ -2869,8 +2883,10 @@ public class AreaProtection implements Listener {
|
||||
// 如果ac为null(不在任何领地内),直接允许
|
||||
if (ac == null) return false;
|
||||
|
||||
// ★ denyPVP和denyAllDamage是全局安全设置,ADMIN/OWNER/VISITOR也不例外
|
||||
boolean isSecurityPerm = "denyPVP".equals(permName) || "denyAllDamage".equals(permName);
|
||||
// ★ denyPVP / denyAllDamage / denyWax 是全局安全设置,ADMIN/OWNER/VISITOR 也不例外
|
||||
boolean isSecurityPerm = "denyPVP".equals(permName)
|
||||
|| "denyAllDamage".equals(permName)
|
||||
|| "denyWax".equals(permName);
|
||||
PermissionLevel level = isSecurityPerm ? null : getPermissionLevel(player, ac);
|
||||
|
||||
// 1) ADMIN/OWNER 永远豁免
|
||||
@@ -2905,6 +2921,32 @@ public class AreaProtection implements Listener {
|
||||
return getLandDefaultDeny(ac, permName);
|
||||
}
|
||||
|
||||
/** ★ 是否为刷怪蛋(生物蛋)物品 */
|
||||
private static boolean isSpawnEggItem(Material m) {
|
||||
return m != null && m.name().endsWith("_SPAWN_EGG");
|
||||
}
|
||||
|
||||
/**
|
||||
* ★ 涂蜡/刮蜡权限是否生效:
|
||||
* 2026-09-27 起改为【独立一级权限】:不再受"破坏方块/放置方块"上级开关约束,
|
||||
* 也不做所有者/管理员/白名单豁免(按 denyPVP 同款"安全权限"处理)——
|
||||
* 原因:此前"上级权限 + 角色豁免"双重条件导致领地开了开关却完全拦不住,
|
||||
* 与"开启即禁止"的预期不符。需要放行特定玩家时,走 per-player 权限单独放行。
|
||||
*/
|
||||
private boolean isWaxDenied(Player player, AreaConfig ac) {
|
||||
if (ac == null) return false;
|
||||
return getEffectiveDeny(player, ac, "denyWax");
|
||||
}
|
||||
|
||||
/** ★ 可涂蜡/刮蜡的方块(铜质方块、避雷针、告示牌) */
|
||||
private static boolean isWaxableBlock(Block b) {
|
||||
if (b == null) return false;
|
||||
String n = b.getType().name();
|
||||
if (n.contains("COPPER") && !n.contains("RAW_COPPER")) return true;
|
||||
if (n.equals("LIGHTNING_ROD")) return true;
|
||||
return n.endsWith("_SIGN");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取领地默认deny状态(按字段名映射)
|
||||
*/
|
||||
@@ -2944,6 +2986,8 @@ public class AreaProtection implements Listener {
|
||||
case "denyAnimalFeeding": return ac.denyAnimalFeeding;
|
||||
case "denyMobAttack": return ac.denyMobAttack;
|
||||
case "denySignEdit": return ac.denySignEdit;
|
||||
case "denySpawnEgg": return ac.denySpawnEgg;
|
||||
case "denyWax": return ac.denyWax;
|
||||
case "peaceMode": return ac.peaceMode;
|
||||
default: return false;
|
||||
}
|
||||
@@ -5078,6 +5122,15 @@ public class AreaProtection implements Listener {
|
||||
loc.getBlockZ());
|
||||
if (ac == null) return;
|
||||
|
||||
// ★ 生物蛋权限:上级权限=放置方块(放置未开启时,单独开启生物蛋不生效)
|
||||
if (isSpawnEggItem(p.getInventory().getItemInMainHand().getType())
|
||||
&& getEffectiveDeny(p, ac, "denyBlockPlace")
|
||||
&& getEffectiveDeny(p, ac, "denySpawnEgg")) {
|
||||
e.setCancelled(true);
|
||||
p.sendMessage("§c§l[区域防护] §f禁止使用生物蛋");
|
||||
return;
|
||||
}
|
||||
|
||||
// 层级:denyBlockPlace (一级) → denyEntityInteract (二级) → denyItemFrame (三级)
|
||||
if (getEffectiveDeny(p, ac, "denyBlockPlace")) {
|
||||
e.setCancelled(true);
|
||||
@@ -5237,6 +5290,72 @@ public class AreaProtection implements Listener {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 涂蜡/刮蜡权限(denyWax)—— 独立处理器 =====
|
||||
// 为什么单独开一个方法:
|
||||
// ① onInteract 开头有选区工具等分支会提前 return,合并写会被短路跳过;
|
||||
// ② 用 e.getItem()/e.getHand() 取"实际使用的那只手",副手蜜脾/副手斧同样拦截;
|
||||
// ③ 同时 DENY useInteractedBlock + useItemInHand,确保原版"刮蜡/涂蜡"流程(1.20.5+ 在
|
||||
// ServerPlayerGameMode.useItemOn 中先查 useItemInHand 再继续 item.useOn)被彻底切断;
|
||||
// ④ HIGHEST 优先级,避免被其它监听器的默认处理覆盖。
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onWaxInteract(PlayerInteractEvent e) {
|
||||
Action act = e.getAction();
|
||||
if (act != Action.RIGHT_CLICK_BLOCK && act != Action.RIGHT_CLICK_AIR) return;
|
||||
// 副手事件会与主手重复触发一次,只处理主手
|
||||
if (e.getHand() == org.bukkit.inventory.EquipmentSlot.OFF_HAND) return;
|
||||
|
||||
Player p = e.getPlayer();
|
||||
ItemStack used = e.getItem();
|
||||
if (used == null || used.getType().isAir()) {
|
||||
used = p.getInventory().getItemInMainHand();
|
||||
}
|
||||
Material mat = used.getType();
|
||||
boolean waxOn = mat == Material.HONEYCOMB;
|
||||
boolean waxOff = mat.name().endsWith("_AXE"); // 木/石/铜/铁/金/钻/下界合金斧全部匹配
|
||||
if (!waxOn && !waxOff) return;
|
||||
|
||||
Block waxBlock = e.getClickedBlock();
|
||||
if (waxBlock == null) {
|
||||
try { waxBlock = p.getTargetBlockExact(6); } catch (Exception ignored) {}
|
||||
}
|
||||
if (waxBlock == null || !isWaxableBlock(waxBlock)) return;
|
||||
|
||||
AreaConfig waxAc = getArea(waxBlock.getWorld().getName(),
|
||||
waxBlock.getX(), waxBlock.getY(), waxBlock.getZ());
|
||||
if (waxAc == null) return; // 不在任何领地内 → 不干预
|
||||
|
||||
if (isWaxDenied(p, waxAc)) {
|
||||
e.setUseInteractedBlock(org.bukkit.event.Event.Result.DENY);
|
||||
e.setUseItemInHand(org.bukkit.event.Event.Result.DENY);
|
||||
e.setCancelled(true);
|
||||
p.sendMessage("§c§l[区域防护] §f禁止" + (waxOn ? "涂蜡" : "刮蜡"));
|
||||
return;
|
||||
}
|
||||
|
||||
// ★ 诊断日志:领地已开启 denyWax 却放行时,把放行原因打进控制台,
|
||||
// 方便定位"为什么没拦住"(所有者/管理员/白名单访客豁免是最常见原因)。
|
||||
if (waxAc.denyWax) {
|
||||
plugin.getLogger().info("[防护] 放行" + (waxOn ? "涂蜡" : "刮蜡")
|
||||
+ ": 玩家=" + p.getName() + " 领地=" + waxAc.name
|
||||
+ " 原因=" + waxAllowReason(p, waxAc));
|
||||
}
|
||||
}
|
||||
|
||||
/** 诊断用:denyWax 已开却放行的原因(改为安全权限后,只剩 per-player 单独放行一条路径) */
|
||||
private String waxAllowReason(Player p, AreaConfig ac) {
|
||||
try {
|
||||
int landId = getLandIdFromDb(ac.name);
|
||||
if (landId > 0) {
|
||||
Map<String, Boolean> perms = getPlayerPermMap(landId, p.getName());
|
||||
if (perms != null && perms.containsKey("denyWax")
|
||||
&& !Boolean.TRUE.equals(perms.get("denyWax"))) {
|
||||
return "该玩家被单独放行(denyWax=false)";
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
return "未知(请反馈该日志)";
|
||||
}
|
||||
|
||||
// 替换整个 onInteract 方法
|
||||
@EventHandler
|
||||
public void onInteract(PlayerInteractEvent e) {
|
||||
@@ -5318,6 +5437,28 @@ public class AreaProtection implements Listener {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 涂蜡/刮蜡权限(denyWax)=====
|
||||
// ★ 已拆成独立处理器 onWaxInteract(见本类 onInteract 方法之前):
|
||||
// ① 本方法开头有多处提前 return(选区工具分支等),合并写会被短路跳过;
|
||||
// ② 独立处理器改用 e.getItem()/e.getHand() 取"实际使用的手",副手蜜脾/斧同样拦截。
|
||||
|
||||
// ===== 生物蛋权限(denySpawnEgg)=====
|
||||
// 上级权限=放置方块:放置未开启时,单独开启本项不生效
|
||||
if (isSpawnEggItem(hand.getType())
|
||||
&& (e.getAction() == Action.RIGHT_CLICK_BLOCK || e.getAction() == Action.RIGHT_CLICK_AIR)) {
|
||||
Block eggBlock = e.getClickedBlock();
|
||||
Location eggLoc = (eggBlock != null) ? eggBlock.getLocation() : p.getLocation();
|
||||
AreaConfig eggAc = getArea(p.getWorld().getName(),
|
||||
eggLoc.getBlockX(), eggLoc.getBlockY(), eggLoc.getBlockZ());
|
||||
if (eggAc != null
|
||||
&& getEffectiveDeny(p, eggAc, "denyBlockPlace")
|
||||
&& getEffectiveDeny(p, eggAc, "denySpawnEgg")) {
|
||||
e.setCancelled(true);
|
||||
p.sendMessage("§c§l[区域防护] §f禁止使用生物蛋");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 实体类物品放置:层级管控 =====
|
||||
// Level 1: denyBlockPlace(一级总开关)→ 禁止一切放置
|
||||
// Level 2: denyEntityInteract(二级实体交互)→ 禁止实体交互
|
||||
@@ -5382,28 +5523,35 @@ public class AreaProtection implements Listener {
|
||||
if (e.getAction() == Action.RIGHT_CLICK_BLOCK) {
|
||||
Block clicked = e.getClickedBlock();
|
||||
if (clicked != null && isUIBlock(clicked.getType())) {
|
||||
// ★ 补刀:被点击方块定位 + 四向相邻同材质半边(双箱被点侧在领地外)
|
||||
// + 上下方向(潜影盒叠放/箱子上下不合并但漏斗矿车场景仍按方块算)
|
||||
// 任一侧命中领地且 denyContainer 即拦截。
|
||||
java.util.List<AreaConfig> cands = new java.util.ArrayList<>();
|
||||
AreaConfig cac = getArea(clicked.getWorld().getName(),
|
||||
clicked.getX(), clicked.getY(), clicked.getZ());
|
||||
if (cac == null && (clicked.getType() == Material.CHEST
|
||||
|| clicked.getType() == Material.TRAPPED_CHEST)) {
|
||||
org.bukkit.block.BlockFace[] horiz = {
|
||||
if (cac != null) cands.add(cac);
|
||||
if (clicked.getType() == Material.CHEST
|
||||
|| clicked.getType() == Material.TRAPPED_CHEST) {
|
||||
org.bukkit.block.BlockFace[] around = {
|
||||
org.bukkit.block.BlockFace.EAST, org.bukkit.block.BlockFace.WEST,
|
||||
org.bukkit.block.BlockFace.NORTH, org.bukkit.block.BlockFace.SOUTH};
|
||||
for (org.bukkit.block.BlockFace bf : horiz) {
|
||||
for (org.bukkit.block.BlockFace bf : around) {
|
||||
Block nb = clicked.getRelative(bf);
|
||||
if (nb.getType() == clicked.getType()) {
|
||||
cac = getArea(nb.getWorld().getName(),
|
||||
AreaConfig na = getArea(nb.getWorld().getName(),
|
||||
nb.getX(), nb.getY(), nb.getZ());
|
||||
if (cac != null) break;
|
||||
if (na != null && !cands.contains(na)) cands.add(na);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 公共设施连控:公共设施启用时访客自动获得容器交互权限
|
||||
if (cac != null && !cac.isPublicBuilding
|
||||
&& getEffectiveDeny(p, cac, "denyContainer")) {
|
||||
e.setCancelled(true);
|
||||
p.sendMessage("§c§l[区域防护] §f此领地禁止访问容器");
|
||||
return;
|
||||
// ★ 公共设施连控:公共设施启用时访客自动获得容器交互权限
|
||||
for (AreaConfig cand : cands) {
|
||||
if (cand.isPublicBuilding) continue;
|
||||
if (getEffectiveDeny(p, cand, "denyContainer")) {
|
||||
e.setCancelled(true);
|
||||
p.sendMessage("§c§l[区域防护] §f此领地禁止访问容器");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5604,6 +5752,17 @@ public class AreaProtection implements Listener {
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 补刀:holder 无法解析出位置(部分容器/插件自定义 holder)时,
|
||||
// 回退用玩家视线命中的方块再定位一次,避免直接放行。
|
||||
if (locs.isEmpty()) {
|
||||
try {
|
||||
Block eye = p.getTargetBlockExact(6);
|
||||
if (eye != null && isUIBlock(eye.getType())) {
|
||||
locs.add(eye.getLocation());
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
|
||||
if (locs.isEmpty()) return;
|
||||
|
||||
// ★ 跨边界大箱子:任一侧在领地内即判定;先取第一个命中领地做打开记录
|
||||
@@ -5627,10 +5786,27 @@ public class AreaProtection implements Listener {
|
||||
plugin.landRecordManager.recordContainerOpen(ac, p, hitLoc, containerType, e.getInventory());
|
||||
|
||||
// ★ 逐侧判定:任一侧所在领地禁止容器访问即拦截(跨边界大箱子不再漏网)
|
||||
// ★ 补刀:某侧查不到领地时,回查四向同材质半边(被开侧在领地外、
|
||||
// 另一半在领地内)与视线命中方块,避免站领地外开领地内箱子漏网。
|
||||
for (Location loc : locs) {
|
||||
if (loc == null || loc.getWorld() == null) continue;
|
||||
AreaConfig a = getArea(loc.getWorld().getName(),
|
||||
loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
|
||||
if (a == null) {
|
||||
Block blk = loc.getBlock();
|
||||
if (blk.getType() == Material.CHEST || blk.getType() == Material.TRAPPED_CHEST) {
|
||||
org.bukkit.block.BlockFace[] around = {
|
||||
org.bukkit.block.BlockFace.EAST, org.bukkit.block.BlockFace.WEST,
|
||||
org.bukkit.block.BlockFace.NORTH, org.bukkit.block.BlockFace.SOUTH};
|
||||
for (org.bukkit.block.BlockFace bf : around) {
|
||||
Block nb = blk.getRelative(bf);
|
||||
if (nb.getType() != blk.getType()) continue;
|
||||
a = getArea(nb.getWorld().getName(),
|
||||
nb.getX(), nb.getY(), nb.getZ());
|
||||
if (a != null) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (a == null) continue;
|
||||
if (hasPermission(p, a, PermissionLevel.OWNER)) continue;
|
||||
|
||||
@@ -5655,11 +5831,22 @@ public class AreaProtection implements Listener {
|
||||
if (side instanceof org.bukkit.inventory.Inventory) {
|
||||
sh = ((org.bukkit.inventory.Inventory) side).getHolder();
|
||||
}
|
||||
// ★ 补刀:半边 holder 仍可能是 DoubleChest(跨边界双箱的其中一侧),递归展开
|
||||
if (sh instanceof org.bukkit.block.DoubleChest) {
|
||||
org.bukkit.block.DoubleChest dc2 = (org.bukkit.block.DoubleChest) sh;
|
||||
addChestSideLocation(locs, dc2.getLeftSide());
|
||||
addChestSideLocation(locs, dc2.getRightSide());
|
||||
return;
|
||||
}
|
||||
if (sh instanceof BlockState) {
|
||||
Location l = ((BlockState) sh).getLocation();
|
||||
if (l != null) locs.add(l);
|
||||
} else if (sh instanceof org.bukkit.block.Block) {
|
||||
locs.add(((org.bukkit.block.Block) sh).getLocation());
|
||||
} else if (sh instanceof Container) {
|
||||
// ★ 补刀:非 BlockState 的 Container(如矿车容器被包装)取其位置
|
||||
Location l = ((Container) sh).getLocation();
|
||||
if (l != null) locs.add(l);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5968,6 +6155,9 @@ public class AreaProtection implements Listener {
|
||||
|| mat == Material.BREWING_STAND
|
||||
|| mat.name().contains("SIGN") || mat.name().contains("BANNER")
|
||||
|| mat == Material.BARREL || mat.name().contains("SHULKER_BOX")
|
||||
// ★ 补刀:漏掉的容器/UI方块(此前这些方块在领地外右键不受 denyContainer 管控)
|
||||
|| mat == Material.CRAFTER || mat.name().contains("BOOKSHELF")
|
||||
|| mat == Material.COMPOSTER || mat.name().contains("CAULDRON")
|
||||
|| mat == Material.ENDER_CHEST || mat.name().contains("CHEST")
|
||||
|| mat.name().contains("FURNACE") || mat.name().contains("BLAST_FURNACE")
|
||||
|| mat.name().contains("SMOKER") || mat == Material.HOPPER
|
||||
@@ -6030,6 +6220,19 @@ public class AreaProtection implements Listener {
|
||||
if (!(e.getPlayer() instanceof Player)) return;
|
||||
Player p = e.getPlayer();
|
||||
Entity entity = e.getRightClicked();
|
||||
|
||||
// ★ 蜜脾涂蜡实体(铜傀儡等):受涂蜡权限管控(独立安全权限,开启即禁止)
|
||||
ItemStack waxHand = p.getInventory().getItemInMainHand();
|
||||
if (waxHand != null && waxHand.getType() == Material.HONEYCOMB) {
|
||||
Location waxLoc = entity.getLocation();
|
||||
AreaConfig waxAc = getArea(waxLoc.getWorld().getName(),
|
||||
waxLoc.getBlockX(), waxLoc.getBlockY(), waxLoc.getBlockZ());
|
||||
if (isWaxDenied(p, waxAc)) {
|
||||
e.setCancelled(true);
|
||||
p.sendMessage("§c§l[区域防护] §f禁止涂蜡");
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 只检测动物类
|
||||
String typeName = entity.getType().name();
|
||||
if (!(entity instanceof Animals || typeName.contains("IRON_GOLEM")
|
||||
@@ -6927,6 +7130,7 @@ public class AreaProtection implements Listener {
|
||||
int cPrice = (cugm != null) ? cugm.getPlayerLandPricePerSqm(cp.getName(), globalCreatePricePerSqm) : globalCreatePricePerSqm;
|
||||
int cCost = cArea * cPrice;
|
||||
// 扣费
|
||||
int chargedCost = 0;
|
||||
if (cCost > 0) {
|
||||
BondManager cbm = plugin.getBonds();
|
||||
if (cbm != null) {
|
||||
@@ -6935,12 +7139,14 @@ public class AreaProtection implements Listener {
|
||||
}
|
||||
String cSrc = (cPrice != globalCreatePricePerSqm) ? "(用户组优惠价)" : "";
|
||||
cp.sendMessage("§a§l[防护] §f创建领地扣除 §e" + cCost + " §f债券(" + cArea + "㎡×" + cPrice + "/㎡)" + cSrc);
|
||||
chargedCost = cCost;
|
||||
}
|
||||
}
|
||||
AreaConfig cac = new AreaConfig();
|
||||
cac.name = confirmName; cac.owner = cp.getName(); cac.world = cl1.getWorld().getName();
|
||||
cac.x1 = cl1.getBlockX(); cac.z1 = cl1.getBlockZ(); cac.x2 = cl2.getBlockX(); cac.z2 = cl2.getBlockZ();
|
||||
cac.yMin = 0; cac.yMax = 255;
|
||||
cac.createCost = chargedCost; // ★ 记录创建扣款(删除时按比例退费的基数)
|
||||
saveAreaToDb(cac);
|
||||
areas.put(confirmName, cac); // ★ 立即更新内存
|
||||
cp.sendMessage("§a§l[防护] §f区域 " + confirmName + " 已创建 (owner: " + cp.getName() + ")");
|
||||
@@ -7154,8 +7360,11 @@ public class AreaProtection implements Listener {
|
||||
sender.sendMessage("§e§l==== 全局配置 ====");
|
||||
sender.sendMessage("§a创建价格(每㎡): §f" + globalCreatePricePerSqm);
|
||||
sender.sendMessage("§a每人最大领地数: §f" + globalMaxLandsPerPlayer);
|
||||
sender.sendMessage("§a删除退费比例(%): §f" + globalRefundRatioPerCent
|
||||
+ (globalRefundRatioPerCent <= 0 ? " §7(已关闭退费)"
|
||||
: (globalRefundRatioPerCent >= 100 ? " §7(全额退款)" : " §7(原价×比例)")));
|
||||
sender.sendMessage("§7用法: /protect config <key> <value>");
|
||||
sender.sendMessage("§7可用key: create_price, max_lands, default_height, peace_duration");
|
||||
sender.sendMessage("§7可用key: create_price, max_lands, default_height, peace_duration, refund_ratio");
|
||||
return true;
|
||||
}
|
||||
String key = args[1];
|
||||
@@ -7178,6 +7387,13 @@ public class AreaProtection implements Listener {
|
||||
int newDuration = parseSmartNumber(value, 3600, 0);
|
||||
updateAreaConfig("peace_mode_max_duration", String.valueOf(newDuration));
|
||||
break;
|
||||
case "refund_ratio":
|
||||
int newRatio = parseSmartNumber(value, globalRefundRatioPerCent, 0);
|
||||
if (newRatio > 100) newRatio = 100; // 上限100%=全额退款
|
||||
if (newRatio < 0) newRatio = 0; // 0=关闭退费
|
||||
globalRefundRatioPerCent = newRatio;
|
||||
updateAreaConfig("refund_ratio_per_cent", String.valueOf(newRatio));
|
||||
break;
|
||||
default:
|
||||
sender.sendMessage("§c未知配置项: " + key);
|
||||
return true;
|
||||
@@ -7814,7 +8030,8 @@ public class AreaProtection implements Listener {
|
||||
}
|
||||
Player p = (Player) sender;
|
||||
if (args.length < 2) {
|
||||
sender.sendMessage("§e用法: /protect 删除 <名>");
|
||||
sender.sendMessage("§e用法: /protect 删除 <名> [-f]");
|
||||
sender.sendMessage("§7-f = 管理员强制删除:跳过60秒倒计时与退费流程,立即删除");
|
||||
return true;
|
||||
}
|
||||
String areaName = args[1];
|
||||
@@ -7832,6 +8049,36 @@ public class AreaProtection implements Listener {
|
||||
sender.sendMessage("§c需要管理员权限或领地所有权");
|
||||
return true;
|
||||
}
|
||||
// ★ 管理员强制删除 -f:跳过60秒倒计时与退费流程,立即删除
|
||||
boolean forceDelete = false;
|
||||
for (String argTok : args) {
|
||||
if (argTok.equalsIgnoreCase("-f") || argTok.equalsIgnoreCase("--force")) {
|
||||
forceDelete = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// ★ 非管理员携带 -f:不报错、不提示,静默回退到普通60秒删除流程
|
||||
if (forceDelete && !isAreaAdmin(sender)) {
|
||||
forceDelete = false;
|
||||
}
|
||||
if (forceDelete) {
|
||||
if (delAc == null) {
|
||||
sender.sendMessage("§c领地数据异常: " + areaName);
|
||||
return true;
|
||||
}
|
||||
cancelPendingDelete(areaName);
|
||||
deleteAreaFromDb(areaName);
|
||||
areas.remove(areaName);
|
||||
plugin.getLogger().warning("[防护] 管理员 " + sender.getName() + " 强制删除领地: " + areaName
|
||||
+ " (owner=" + delAc.owner + ", 未退费, 坐标 " + delAc.world + " "
|
||||
+ delAc.x1 + "," + delAc.z1 + " ~ " + delAc.x2 + "," + delAc.z2 + ")");
|
||||
sender.sendMessage("§a§l[防护] §f已强制删除领地 §e" + areaName + " §7(跳过倒计时与退费)");
|
||||
reportLandRemovalToAdmins("§6§l[防护-强制删除] §f" + sender.getName()
|
||||
+ " 强制删除领地 §e" + areaName + " §7(owner=" + delAc.owner
|
||||
+ ", 不退款) [" + delAc.world + " " + delAc.x1 + "," + delAc.z1
|
||||
+ " ~ " + delAc.x2 + "," + delAc.z2 + "]");
|
||||
return true;
|
||||
}
|
||||
// ★ 检查是否已有待删除请求
|
||||
PendingDelete existing = pendingDeletes.get(areaName);
|
||||
if (existing != null) {
|
||||
@@ -7848,6 +8095,7 @@ public class AreaProtection implements Listener {
|
||||
// 也显示到领地列表(CLI/GUI取消按钮会自动刷新)
|
||||
UUID uid = p.getUniqueId();
|
||||
String finalAreaName = areaName;
|
||||
final String finalOperator = p.getName();
|
||||
long start = System.currentTimeMillis();
|
||||
BukkitTask task = new BukkitRunnable() {
|
||||
@Override
|
||||
@@ -7855,6 +8103,8 @@ public class AreaProtection implements Listener {
|
||||
pendingDeletes.remove(finalAreaName);
|
||||
// ★ 60秒到期后自动删除领地
|
||||
if (areas.containsKey(finalAreaName)) {
|
||||
// ★ 到期删除:按创建扣款比例退费(0%=关闭退费)
|
||||
refundOnLandDelete(areas.get(finalAreaName), finalOperator, "删除领地退费: " + finalAreaName);
|
||||
deleteAreaFromDb(finalAreaName);
|
||||
areas.remove(finalAreaName);
|
||||
plugin.getLogger().info("[防护] 领地 §e" + finalAreaName + " §7等待期结束,已自动删除");
|
||||
@@ -9028,6 +9278,143 @@ public class AreaProtection implements Listener {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* ★ 删除领地时按创建扣款比例退费
|
||||
* 退费比例读取 area_config.refund_ratio_per_cent:
|
||||
* 0 = 关闭退费(不退)
|
||||
* 100 = 全额退款(原价 100%)
|
||||
* 其他= 原价 × 比例%
|
||||
* 债券走 BondManager.addBonds(记 land_refund 流水)
|
||||
*/
|
||||
private void refundOnLandDelete(AreaConfig ac, String operator, String reason) {
|
||||
if (ac == null) return;
|
||||
int ratio = globalRefundRatioPerCent;
|
||||
if (ratio <= 0) return; // 0 = 关闭退费
|
||||
if (ratio > 100) ratio = 100; // 上限 100%
|
||||
if (ac.createCost <= 0) return; // 未扣款(历史数据/管理员创建)不退
|
||||
int refund = (int) Math.floor(ac.createCost * (ratio / 100.0));
|
||||
if (refund <= 0) return;
|
||||
BondManager bm = plugin.getBonds();
|
||||
if (bm == null) return;
|
||||
String op = (operator == null || operator.isEmpty()) ? "system" : operator;
|
||||
int after = bm.addBonds(ac.owner, refund, "land_refund", ac.owner, op, reason);
|
||||
if (after < 0) {
|
||||
plugin.getLogger().warning("[防护] 领地退费失败(账户冻结?): " + ac.owner + " +" + refund);
|
||||
return;
|
||||
}
|
||||
plugin.getLogger().info("[防护] 领地 " + ac.name + " 删除退费: " + ac.owner
|
||||
+ " +" + refund + " (原价 " + ac.createCost + " × " + ratio + "%)");
|
||||
Player ownerOnline = Bukkit.getPlayerExact(ac.owner);
|
||||
if (ownerOnline != null && ownerOnline.isOnline()) {
|
||||
ownerOnline.sendMessage("§a§l[防护] §f领地 §e" + ac.name + " §f已删除,退还债券 §e" + refund
|
||||
+ " §7(创建原价 " + ac.createCost + " × " + ratio + "%)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ★ 向在线的插件管理员 & OP 上报领地强制删除信息(含坐标)
|
||||
*/
|
||||
private void reportLandRemovalToAdmins(String message) {
|
||||
int sent = 0;
|
||||
for (Player online : Bukkit.getOnlinePlayers()) {
|
||||
if (online.isOp() || isAreaAdmin(online)) {
|
||||
online.sendMessage(message);
|
||||
sent++;
|
||||
}
|
||||
}
|
||||
plugin.getLogger().info("[防护] 已上报 " + sent + " 名在线管理员: " + message.replaceAll("§.", ""));
|
||||
}
|
||||
|
||||
// ==================== 封禁/注销领主的领地自动清理 ====================
|
||||
|
||||
private BukkitTask bannedOwnerSweepTask;
|
||||
/** 扫描周期:5分钟 */
|
||||
private static final long SWEEP_INTERVAL_TICKS = 6000L;
|
||||
/** 剩余封禁时长 ≥ 9年,视为"10年封禁" */
|
||||
private static final long LONG_BAN_REMAIN_MS = 9L * 365 * 24 * 3600 * 1000L;
|
||||
|
||||
/**
|
||||
* ★ 启动封禁/注销领主的领地自动清理任务
|
||||
* 命中以下任一条件的领地 → 立即强制删除(不退款、不注销账号),
|
||||
* 并把领地坐标上报给在线的插件管理员 & OP:
|
||||
* 1) 领主被永久封禁
|
||||
* 2) 领主被 10 年封禁
|
||||
* 3) 领主账户已注销
|
||||
*/
|
||||
public void startBannedOwnerSweepTask() {
|
||||
if (bannedOwnerSweepTask != null) {
|
||||
bannedOwnerSweepTask.cancel();
|
||||
}
|
||||
bannedOwnerSweepTask = new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
sweepBannedOwnerLands();
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[防护] 领地自动清理任务异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}.runTaskTimer(plugin, 200L, SWEEP_INTERVAL_TICKS);
|
||||
}
|
||||
|
||||
public void stopBannedOwnerSweepTask() {
|
||||
if (bannedOwnerSweepTask != null) {
|
||||
bannedOwnerSweepTask.cancel();
|
||||
bannedOwnerSweepTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void sweepBannedOwnerLands() {
|
||||
if (areas.isEmpty()) return;
|
||||
DatabaseManager dbMgr = plugin.getDb();
|
||||
for (AreaConfig ac : new ArrayList<>(areas.values())) {
|
||||
if (ac == null || ac.owner == null || ac.owner.isEmpty()) continue;
|
||||
// 快照遍历期间已被删除/重载的跳过
|
||||
if (areas.get(ac.name) != ac) continue;
|
||||
|
||||
String reason = null;
|
||||
|
||||
// 1/2) 永久封禁 / 10年封禁
|
||||
try {
|
||||
org.bukkit.BanEntry<?> ban =
|
||||
Bukkit.getBanList(org.bukkit.BanList.Type.NAME).getBanEntry(ac.owner);
|
||||
if (ban != null) {
|
||||
java.util.Date exp = ban.getExpiration();
|
||||
if (exp == null) {
|
||||
reason = "永久封禁";
|
||||
} else if (exp.getTime() - System.currentTimeMillis() >= LONG_BAN_REMAIN_MS) {
|
||||
reason = "10年封禁";
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
// 3) 账户已注销(在线领主一律跳过,避免误删)
|
||||
if (reason == null) {
|
||||
if (dbMgr == null) continue;
|
||||
Player ownerOnline = Bukkit.getPlayerExact(ac.owner);
|
||||
if (ownerOnline != null && ownerOnline.isOnline()) continue;
|
||||
try {
|
||||
if (!dbMgr.userExists(ac.owner)) {
|
||||
reason = "账户已注销";
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
if (reason == null) continue;
|
||||
|
||||
// 不退款、不注销账号,直接强制删除
|
||||
cancelPendingDelete(ac.name);
|
||||
deleteAreaFromDb(ac.name);
|
||||
areas.remove(ac.name);
|
||||
String coord = ac.world + " " + ac.x1 + "," + ac.z1 + " ~ " + ac.x2 + "," + ac.z2;
|
||||
plugin.getLogger().warning("[防护] 自动清理领地 " + ac.name + ":领主 " + ac.owner
|
||||
+ " 因" + reason + ",已强制删除(不退款)坐标 " + coord);
|
||||
reportLandRemovalToAdmins("§6§l[防护-自动清理] §f领主 §e" + ac.owner
|
||||
+ " §f因§c" + reason + " §f已自动删除其领地 §e" + ac.name
|
||||
+ " §7(不退款) [" + coord + "]");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果领地有待删除请求,取消它(玩家操作领地时自动解除冻结)
|
||||
*/
|
||||
@@ -9150,8 +9537,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) "
|
||||
+ "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) "
|
||||
+ "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, "
|
||||
@@ -9164,7 +9551,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");
|
||||
+ "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");
|
||||
|
||||
stmt.setString(1, ac.name);
|
||||
stmt.setString(2, ac.owner != null ? ac.owner : "");
|
||||
@@ -9235,6 +9622,9 @@ public class AreaProtection implements Listener {
|
||||
stmt.setInt(67, ac.allowVisitorTeleport ? 1 : 0);
|
||||
stmt.setInt(68, ac.denyFarmlandTrample ? 1 : 0);
|
||||
stmt.setInt(69, ac.denyEnderTeleport ? 1 : 0);
|
||||
stmt.setInt(70, ac.createCost);
|
||||
stmt.setInt(71, ac.denySpawnEgg ? 1 : 0);
|
||||
stmt.setInt(72, ac.denyWax ? 1 : 0);
|
||||
stmt.executeUpdate();
|
||||
stmt.close();
|
||||
// ★ 领地设置变更:立即触发PHP同步(防抖10秒)
|
||||
@@ -9639,6 +10029,8 @@ public class AreaProtection implements Listener {
|
||||
case "deny_crop_harvest": ac.denyCropHarvest = "1".equals(value); break;
|
||||
case "deny_wool_shear": ac.denyWoolShear = "1".equals(value); break;
|
||||
case "deny_animal_feeding": ac.denyAnimalFeeding = "1".equals(value); break;
|
||||
case "deny_spawn_egg": ac.denySpawnEgg = "1".equals(value); break;
|
||||
case "deny_wax": ac.denyWax = "1".equals(value); break;
|
||||
case "deny_mob_attack": ac.denyMobAttack = "1".equals(value); break;
|
||||
case "deny_item_frame": ac.denyItemFrame = "1".equals(value); break;
|
||||
case "deny_entity_interact": ac.denyEntityInteract = "1".equals(value); break;
|
||||
@@ -9846,6 +10238,7 @@ public class AreaProtection implements Listener {
|
||||
case "max_lands_per_player": globalMaxLandsPerPlayer = Integer.parseInt(value); break;
|
||||
case "max_home_per_player": globalMaxHomePerPlayer = Integer.parseInt(value); break;
|
||||
case "default_height": globalDefaultHeight = Integer.parseInt(value); break;
|
||||
case "refund_ratio_per_cent": globalRefundRatioPerCent = Integer.parseInt(value); break;
|
||||
}
|
||||
} catch (NumberFormatException ignored) {}
|
||||
} catch (Exception e) {
|
||||
@@ -9933,6 +10326,12 @@ public class AreaProtection implements Listener {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ★ 退费比例范围钳制:0=关闭,100=全额
|
||||
if ("refund_ratio_per_cent".equals(configKey)) {
|
||||
if (newValue > 100) newValue = 100;
|
||||
if (newValue < 0) newValue = 0;
|
||||
}
|
||||
|
||||
// 读取当前值
|
||||
int currentValue = 0;
|
||||
try {
|
||||
@@ -9948,6 +10347,7 @@ public class AreaProtection implements Listener {
|
||||
case "create_price_per_sqm": globalCreatePricePerSqm = newValue; break;
|
||||
case "max_lands_per_player": globalMaxLandsPerPlayer = newValue; break;
|
||||
case "default_height": globalDefaultHeight = newValue; break;
|
||||
case "refund_ratio_per_cent": globalRefundRatioPerCent = newValue; break;
|
||||
}
|
||||
|
||||
String configName = getConfigNameByKey(configKey);
|
||||
@@ -10409,6 +10809,7 @@ public class AreaProtection implements Listener {
|
||||
case "max_lands_per_player": return "每人最大领地数";
|
||||
case "default_height": return "默认高度";
|
||||
case "peace_mode_max_duration": return "和平模式最大时长(秒)";
|
||||
case "refund_ratio_per_cent": return "删除退费比例(%)";
|
||||
default: return key;
|
||||
}
|
||||
}
|
||||
@@ -11831,6 +12232,10 @@ public class AreaProtection implements Listener {
|
||||
public boolean denyWoolShear = false;
|
||||
// ★ 投喂动物
|
||||
public boolean denyAnimalFeeding = false;
|
||||
// ★ 生物蛋(右键刷怪蛋生成实体)——上级权限:放置方块(放置未开启时单独开启本项不生效)
|
||||
public boolean denySpawnEgg = false;
|
||||
// ★ 涂蜡/刮蜡(蜜脾涂蜡、斧头刮蜡)——独立安全权限:开启即对所有人(含所有者/管理员/访客)禁止
|
||||
public boolean denyWax = false;
|
||||
// ★ 玩家攻击生物
|
||||
public boolean denyMobAttack = false;
|
||||
public boolean enableAnnounce = false;
|
||||
@@ -11859,6 +12264,8 @@ public class AreaProtection implements Listener {
|
||||
public boolean isPublicBuilding = false;
|
||||
// ★ 访客传送权限(默认关闭:未授权访客不能传送进入领地)
|
||||
public boolean allowVisitorTeleport = false;
|
||||
// ★ 创建领地时实际扣款金额(删除领地按比例退费的基数;0=无扣款)
|
||||
public int createCost = 0;
|
||||
|
||||
|
||||
|
||||
@@ -11975,6 +12382,8 @@ public class AreaProtection implements Listener {
|
||||
if (denyCropHarvest) c++;
|
||||
if (denyWoolShear) c++;
|
||||
if (denyAnimalFeeding) c++;
|
||||
if (denySpawnEgg) c++;
|
||||
if (denyWax) c++;
|
||||
if (!peaceWhitelist.isEmpty()) c++;
|
||||
|
||||
return c;
|
||||
@@ -12056,6 +12465,7 @@ public class AreaProtection implements Listener {
|
||||
}
|
||||
|
||||
// ★ 扣费
|
||||
int chargedCost = 0;
|
||||
if (bm != null && totalCost > 0) {
|
||||
String src = (pricePerSqm != globalCreatePricePerSqm) ? "(用户组优惠价)" : "";
|
||||
if (!bm.deductBonds(p.getName(), totalCost, "land_create", p.getName(), p.getName(),
|
||||
@@ -12064,6 +12474,7 @@ public class AreaProtection implements Listener {
|
||||
return false;
|
||||
}
|
||||
p.sendMessage("§a§l[防护] §f创建领地扣除 §e" + totalCost + " §f债券(" + area + "㎡×" + pricePerSqm + "/㎡)" + src);
|
||||
chargedCost = totalCost;
|
||||
}
|
||||
|
||||
// ★ 创建领地
|
||||
@@ -12073,6 +12484,7 @@ public class AreaProtection implements Listener {
|
||||
ac.world = p.getWorld().getName();
|
||||
ac.x1 = x1; ac.z1 = z1; ac.x2 = x2; ac.z2 = z2;
|
||||
ac.yMin = 0; ac.yMax = 255;
|
||||
ac.createCost = chargedCost; // ★ 记录创建扣款(删除时按比例退费的基数)
|
||||
saveAreaToDb(ac);
|
||||
areas.put(autoName, ac);
|
||||
loadAllAreas();
|
||||
|
||||
@@ -109,7 +109,7 @@ public class ChatFilterManager {
|
||||
|
||||
static class VerificationData {
|
||||
long createTime;
|
||||
int a, b, op; // 0=加法, 1=减法, 2=乘法(九九表), 3=除法(九九表)
|
||||
int a, b, op; // 0=加法, 1=减法, 2=乘法, 3=除法(均为99以内小学运算)
|
||||
String guiTargets; // GUI模式下需要选择的物品名列表("|"分隔)
|
||||
VerificationType type; // 验证码类型
|
||||
boolean completed;
|
||||
@@ -188,7 +188,7 @@ public class ChatFilterManager {
|
||||
|
||||
/**
|
||||
* 生成随机验证码:加 / 减 / 乘 / 除 / GUI 五种每次随机抽一种。
|
||||
* 乘法与除法严格取自九九乘法表。
|
||||
* ★ 算术题统一为小学水平「99以内加减乘除」(和/积≤99、减法不为负、除法整除)。
|
||||
*
|
||||
* ★ 出题前必须先关掉可能还开着的旧验证码GUI:旧GUI 的 InventoryCloseEvent
|
||||
* 会清 verificationData,让它先跑完,否则刚生成的新题会被清掉(点哪儿都没用)。
|
||||
@@ -201,6 +201,10 @@ public class ChatFilterManager {
|
||||
}
|
||||
|
||||
int kind = rand.nextInt(5); // 0加 1减 2乘 3除 4GUI
|
||||
// ★ 基岩版特性:没法用聊天框唤醒GUI验证码 → 基岩版固定出算术题
|
||||
if (kind == 4 && online != null && isBedrockPlayer(online)) {
|
||||
kind = rand.nextInt(4); // 0加 1减 2乘 3除
|
||||
}
|
||||
if (kind == 4) {
|
||||
generateGUIChallenge(playerName, rand);
|
||||
} else {
|
||||
@@ -208,6 +212,20 @@ public class ChatFilterManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** ★ 基岩版玩家判定:UUID特征 + Geyser/Floodgate API 兜底 */
|
||||
private boolean isBedrockPlayer(Player p) {
|
||||
if (!(plugin instanceof Main)) return false;
|
||||
Main main = (Main) plugin;
|
||||
try {
|
||||
VerificationManager vm = main.getVerification();
|
||||
if (vm != null && vm.isBedrockPlayer(p)) return true;
|
||||
TeleportManager tm = main.getTeleport();
|
||||
return tm != null && tm.isBedrockPlayer(p);
|
||||
} catch (Throwable t) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 关掉还开着的验证码GUI(标题匹配才关,避免误关玩家自己的箱子/菜单) */
|
||||
private void closeVerificationGuiIfOpen(Player p) {
|
||||
try {
|
||||
@@ -219,26 +237,30 @@ public class ChatFilterManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** 生成数学题验证码(op: 0加 1减 2乘 3除;乘/除每次从九九乘法表随机抽一题) */
|
||||
/**
|
||||
* 生成数学题验证码(op: 0加 1减 2乘 3除)
|
||||
* ★ 小学水平「99以内加减乘除」:加法和≤99、减法不出现负数、乘法积≤99、除法整除且被除数≤99
|
||||
*/
|
||||
private void generateMathChallenge(String playerName, int op, Random rand) {
|
||||
int a, b;
|
||||
|
||||
switch (op) {
|
||||
case 0: // 两位数加法
|
||||
a = 10 + rand.nextInt(90);
|
||||
b = 10 + rand.nextInt(90);
|
||||
case 0: // 加法:两数之和不超过99
|
||||
a = 1 + rand.nextInt(98); // a ∈ [1, 98]
|
||||
b = 1 + rand.nextInt(99 - a); // b ∈ [1, 99-a] → a+b ≤ 99
|
||||
break;
|
||||
case 1: // 两位数减法(保证结果仍是两位数,不出现负数)
|
||||
a = 20 + rand.nextInt(80);
|
||||
b = 10 + rand.nextInt(a - 19); // b ∈ [10, a-10],结果 ≥ 10
|
||||
case 1: // 减法:被减数不超过99,结果≥1(小学不出现负数/零)
|
||||
a = 2 + rand.nextInt(98); // a ∈ [2, 99]
|
||||
b = 1 + rand.nextInt(a - 1); // b ∈ [1, a-1] → 结果 ∈ [1, a-1]
|
||||
break;
|
||||
case 2: // 九九乘法表:1×1 ~ 9×9
|
||||
a = 1 + rand.nextInt(9);
|
||||
b = 1 + rand.nextInt(9);
|
||||
case 2: // 乘法:积不超过99(如 12×7=84、11×9=99)
|
||||
b = 2 + rand.nextInt(8); // b ∈ [2, 9]
|
||||
a = 2 + rand.nextInt((99 / b) - 1); // a ∈ [2, floor(99/b)] → a*b ≤ 99
|
||||
break;
|
||||
default: // 九九乘法表除法(整除):(i×j) ÷ j = i
|
||||
b = 1 + rand.nextInt(9);
|
||||
a = (1 + rand.nextInt(9)) * b;
|
||||
default: // 除法:整除且被除数不超过99(如 96÷8=12、99÷9=11)
|
||||
b = 2 + rand.nextInt(8); // b ∈ [2, 9]
|
||||
int q = 2 + rand.nextInt((99 / b) - 1); // 商 ∈ [2, floor(99/b)]
|
||||
a = b * q; // a ≤ 99
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -466,7 +488,7 @@ public class ChatFilterManager {
|
||||
case 0 -> expected = vd.a + vd.b;
|
||||
case 1 -> expected = vd.a - vd.b;
|
||||
case 2 -> expected = vd.a * vd.b;
|
||||
default -> expected = vd.a / vd.b; // 九九表整除
|
||||
default -> expected = vd.a / vd.b; // 生成时保证整除(a % b == 0)
|
||||
}
|
||||
|
||||
if (ans == expected) {
|
||||
|
||||
@@ -288,6 +288,10 @@ public class Main extends JavaPlugin
|
||||
return verification;
|
||||
}
|
||||
|
||||
public TeleportManager getTeleport() {
|
||||
return teleportMgr;
|
||||
}
|
||||
|
||||
public RiskControlManager getRiskControl() {
|
||||
return riskControl;
|
||||
}
|
||||
@@ -776,7 +780,8 @@ public class Main extends JavaPlugin
|
||||
"tempban", "arena",
|
||||
"join", "leave",
|
||||
"exit", "test",
|
||||
"equip", "adjust"};
|
||||
"equip", "adjust",
|
||||
"see"};
|
||||
for (String s : sub) {
|
||||
if (s.startsWith(
|
||||
args[0]
|
||||
@@ -810,6 +815,16 @@ public class Main extends JavaPlugin
|
||||
list.add(rn);
|
||||
}
|
||||
}
|
||||
} else if (sub.equals("join")
|
||||
|| sub.equals("arena")
|
||||
|| sub.equals("leave")
|
||||
|| sub.equals("exit")) {
|
||||
// ★ /pvp join see、/pvp leave see —— 观赛模式
|
||||
if ("see".startsWith(
|
||||
args[1]
|
||||
.toLowerCase())) {
|
||||
list.add("see");
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
@@ -6201,8 +6216,8 @@ public class Main extends JavaPlugin
|
||||
sender.sendMessage("§cWeb通信管理器未初始化");
|
||||
return true;
|
||||
}
|
||||
webManager.clearQueue();
|
||||
sender.sendMessage("§a已清空DB队列中未回应的web同步任务");
|
||||
String clearSummary = webManager.clearQueue();
|
||||
sender.sendMessage("§a已清空DB队列: §f" + clearSummary);
|
||||
return true;
|
||||
}
|
||||
// /menu 开关雪球菜单
|
||||
@@ -7932,7 +7947,7 @@ public class Main extends JavaPlugin
|
||||
// ★ config: 第三层是配置key
|
||||
if (sub.equals("config")) {
|
||||
return filterTab(
|
||||
Arrays.asList("create_price", "max_lands", "default_height", "peace_duration"),
|
||||
Arrays.asList("create_price", "max_lands", "default_height", "peace_duration", "refund_ratio"),
|
||||
args[2]);
|
||||
}
|
||||
// ★ cli manage/members/visitorperm/toggle/memberperm/playerperm/toggleplayerperm/clearplayerperm: 第三层是领地名
|
||||
@@ -7962,7 +7977,8 @@ public class Main extends JavaPlugin
|
||||
"fire", "fire_spread", "pickup", "drop", "explosion",
|
||||
"fall_damage", "hunger", "all_damage", "all_effects",
|
||||
"item_frame", "redstone", "door", "audio", "lead",
|
||||
"crop_harvest", "wool_shear", "animal_feed", "glowing", "peace_mode"),
|
||||
"crop_harvest", "wool_shear", "animal_feed", "glowing", "peace_mode",
|
||||
"spawn_egg", "wax"),
|
||||
args[3]);
|
||||
}
|
||||
if (action.equals("manage") || action.equals("members")
|
||||
@@ -7999,7 +8015,8 @@ public class Main extends JavaPlugin
|
||||
"denyFire", "denyFireSpread", "denyPickup", "denyDrop", "denyExplosion",
|
||||
"denyFallDamage", "denyHunger", "denyAllDamage", "denyAllEffects",
|
||||
"denyItemFrame", "denyRedstoneInteraction", "denyDoorInteraction", "denyNoteblockJukebox",
|
||||
"denyLead", "denyCropHarvest", "denyWoolShear", "denyAnimalFeeding", "denyGlowing", "peaceMode"),
|
||||
"denyLead", "denyCropHarvest", "denyWoolShear", "denyAnimalFeeding", "denyGlowing", "peaceMode",
|
||||
"denySpawnEgg", "denyWax"),
|
||||
args[4]);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1123,6 +1123,11 @@ public class PVPManager implements Listener {
|
||||
p, args);
|
||||
case "join":
|
||||
case "arena":
|
||||
// ★ /pvp join see → 直接进入观赛(默认幽灵形态)
|
||||
if (args.length >= 2 && args[1].equalsIgnoreCase("see")) {
|
||||
plugin.getPVPArenaManager().handleSeeCommand(p, "enter");
|
||||
return true;
|
||||
}
|
||||
plugin.getPVPArenaManager().joinArena(p);
|
||||
return true;
|
||||
case "equip":
|
||||
@@ -1133,8 +1138,17 @@ public class PVPManager implements Listener {
|
||||
}
|
||||
plugin.getPVPArenaManager().openEquipmentSelection(p);
|
||||
return true;
|
||||
case "see":
|
||||
// PVP观赛:/pvp see=在裁判⇄幽灵两种观赛形态间切换(仅PVP世界,绝不切回无保护的普通玩家)
|
||||
plugin.getPVPArenaManager().handleSeeCommand(p);
|
||||
return true;
|
||||
case "leave":
|
||||
case "exit":
|
||||
// ★ /pvp leave see → 彻底退出观赛:恢复身份并重新触发选装、补发PVP装备
|
||||
if (args.length >= 2 && args[1].equalsIgnoreCase("see")) {
|
||||
plugin.getPVPArenaManager().handleSeeCommand(p, "exit");
|
||||
return true;
|
||||
}
|
||||
plugin.leavePVP(p);
|
||||
return true;
|
||||
default:
|
||||
@@ -1142,6 +1156,9 @@ public class PVPManager implements Listener {
|
||||
p.sendMessage("§7——— PVP指令 ———");
|
||||
p.sendMessage("§e/pvp join §7进入PVP竞技场");
|
||||
p.sendMessage("§e/pvp equip §7调整装备(食物/盾牌/弓弩)");
|
||||
p.sendMessage("§e/pvp see §7观赛:切换裁判⇄幽灵形态(仅PVP世界)");
|
||||
p.sendMessage("§e/pvp join see §7加入观赛:进入幽灵形态");
|
||||
p.sendMessage("§e/pvp leave see §7退出观赛:重新选装并补发PVP装备");
|
||||
p.sendMessage("§e/pvp leave §7离开PVP竞技场");
|
||||
p.sendMessage("§e/pvp stats [玩家] §7查看战绩");
|
||||
p.sendMessage("§e/pvp list §7查看PVP区域列表");
|
||||
|
||||
@@ -159,8 +159,9 @@ public class PVPTestManager implements Listener {
|
||||
* FLAT 世界生成极快,不会卡服。
|
||||
*/
|
||||
public void ensurePVPTestWorldExists() {
|
||||
File folder = new File(Bukkit.getWorldContainer(), PVP_TEST_WORLD_NAME);
|
||||
boolean fresh = !folder.exists();
|
||||
// ★ 兼容 1.20.4+ 新存储布局(world/dimensions/minecraft/<名字>),旧的根目录拼接会恒定 exists()==false
|
||||
File folder = PVPArenaManager.resolveWorldFolder(PVP_TEST_WORLD_NAME);
|
||||
boolean fresh = folder == null || !folder.exists();
|
||||
|
||||
World w = Bukkit.getWorld(PVP_TEST_WORLD_NAME);
|
||||
if (w == null) {
|
||||
|
||||
@@ -258,7 +258,8 @@ public class TeleportManager implements Listener {
|
||||
/**
|
||||
* 判断是否为基岩版玩家(通过Geyser/Floodgate API)
|
||||
*/
|
||||
private boolean isBedrockPlayer(Player player) {
|
||||
// ★ 公开:验证码(ChatFilterManager)复用此判定,保证基岩版不出GUI题
|
||||
public boolean isBedrockPlayer(Player player) {
|
||||
// 检查 Geyser-Spigot 插件
|
||||
Plugin geyserPlugin = Bukkit.getPluginManager().getPlugin("Geyser-Spigot");
|
||||
if (geyserPlugin == null || !geyserPlugin.isEnabled()) {
|
||||
@@ -1144,7 +1145,19 @@ public class TeleportManager implements Listener {
|
||||
count++;
|
||||
}
|
||||
|
||||
player.sendMessage("§a[传送] 已向 §e" + count + " §a名玩家发送传送请求");
|
||||
if (isBedrockPlayer(player)) {
|
||||
player.sendMessage("§a[传送] 已向 §e" + count + " §a名玩家发送传送请求");
|
||||
player.sendMessage("§7使用 §e/tpacancel §7撤回全部请求");
|
||||
} else {
|
||||
player.sendMessage(Component.empty()
|
||||
.append(Component.text("§a[传送] 已向 §e" + count + " §a名玩家发送传送请求")));
|
||||
// ★ 撤回按钮:发错了可立即撤销,避免更多人接受
|
||||
player.sendMessage(Component.empty()
|
||||
.append(Component.text("§7全服玩家接受前 "))
|
||||
.append(Component.text("§c[↩ 撤回全部请求]")
|
||||
.clickEvent(ClickEvent.runCommand("/tpacancel"))
|
||||
.hoverEvent(HoverEvent.showText(Component.text("立即撤回这次 tpaall 的全部待处理请求")))));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1577,7 +1590,8 @@ public class TeleportManager implements Listener {
|
||||
case 4: // 全服传送
|
||||
event.setCancelled(true);
|
||||
handleTPAll(player);
|
||||
player.closeInventory();
|
||||
// ★ 发送后重开面板:slot 5「取消已发出的请求」立即可用作撤回入口
|
||||
openTeleportPanel(player);
|
||||
break;
|
||||
case 5: // 取消已发出请求
|
||||
event.setCancelled(true);
|
||||
|
||||
@@ -327,8 +327,14 @@ public class WebManager {
|
||||
/**
|
||||
* ★ 清空DB队列中所有未回应的web同步任务
|
||||
* (清空队列/cleartake 命令调用)
|
||||
*
|
||||
* v2:以前只清 Java 内存队列,PHP 侧 web_login_requests 表里的 pending 记录
|
||||
* 原封不动 → 下一轮轮询又原样取回来,看起来"打了几十次都没清掉"。
|
||||
* 现在增加两件事:
|
||||
* ① 复位本地"已处理"标记与TimerA在途计数(避免清完后仍被跳过/卡死)
|
||||
* ② 异步调用 PHP clear_pending_web_logins,把服务端 pending 一并清理
|
||||
*/
|
||||
public void clearQueue() {
|
||||
public String clearQueue() {
|
||||
int count = 0;
|
||||
Iterator<DbTask> it = dbTaskQueue.iterator();
|
||||
while (it.hasNext()) {
|
||||
@@ -342,7 +348,33 @@ public class WebManager {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
plugin.getLogger().info("[DB队列] 清空队列: 丢弃 " + count + " 个未回应web任务");
|
||||
// ★ 复位轮询在途标记:队列里的任务被丢弃后,finally不会执行,必须手工复位
|
||||
processedWebLoginRequests.clear();
|
||||
plugin.getLogger().info("[DB队列] 清空队列: 丢弃 " + count + " 个未回应web任务,联动清理PHP待处理请求...");
|
||||
|
||||
// ★ 联动清理 PHP 侧 pending(异步,不阻塞命令线程)
|
||||
purgePhpPendingWebLogins(count);
|
||||
return "丢弃 " + count + " 个未回应web任务 + 联动清理PHP待处理登录请求";
|
||||
}
|
||||
|
||||
/**
|
||||
* ★ 联动清理 PHP 的 web_login_requests pending 记录
|
||||
* 必须使用 lambda 而非匿名内部类:jar 热替换后匿名内部类会抛
|
||||
* NoClassDefFoundError(WebManager$N 找不到),导致回写/清理永远失败。
|
||||
*/
|
||||
private void purgePhpPendingWebLogins(final int localDropped) {
|
||||
if (!enabled) return;
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
try {
|
||||
String urlStr = webBaseUrl + "/api/sync.php?action=clear_pending_web_logins&secret="
|
||||
+ java.net.URLEncoder.encode(secretKey, "UTF-8");
|
||||
String resp = doGet(urlStr);
|
||||
plugin.getLogger().info("[DB队列] 清空队列联动: 本地丢弃=" + localDropped
|
||||
+ " PHP响应=" + (resp == null ? "null" : resp.substring(0, Math.min(200, resp.length()))));
|
||||
} catch (Throwable t) {
|
||||
plugin.getLogger().warning("[DB队列] 联动清理PHP待处理登录请求失败: " + t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1847,38 +1879,34 @@ public class WebManager {
|
||||
|
||||
// ★ CDK离线兑付:Timer A永不暂停,定期拉取CDK交易
|
||||
// 解决全员下线时Timer B暂停导致CDK兑换不到账的bug
|
||||
//
|
||||
// ★★★ 队列保护(v2.93):原来这一轮会一口气投出【5个】子任务,
|
||||
// 而 db-worker 是单线程、每个任务又含10~30秒级HTTP请求,
|
||||
// Timer A 却每3~5秒就再投一轮 → 队列越堆越长
|
||||
// (日志表现为「等待过久 10s+」「清空队列丢弃17个」依然慢)。
|
||||
// 现在合并为【1个】队列任务串行执行:执行顺序与原来一致
|
||||
// (单线程本来就是串行),但队列条目数降为 1/5。
|
||||
if (timerACycleCount % 5 == 0) { // 每5轮(~15-25秒)检查一次
|
||||
submitNormalDbTask("TimerA-cdkPull", () -> {
|
||||
submitNormalDbTask("TimerA-周期批处理", () -> {
|
||||
// 本地CDK(web_transactions pending)
|
||||
try {
|
||||
// 本地CDK(web_transactions pending)
|
||||
doTransactionPollCheck();
|
||||
} catch (Exception e) { /* 静默 */ }
|
||||
});
|
||||
// 远程CDK(cdk_validate_requests,sdf1计分板CDK验证)
|
||||
if (initialSyncComplete) {
|
||||
submitNormalDbTask("TimerA-cdkRemotePull", () -> {
|
||||
try {
|
||||
pullWebCdkRequestsAndValidate();
|
||||
pullSdf1PendingAndValidateWeb();
|
||||
} catch (Exception e) { /* 静默 */ }
|
||||
});
|
||||
}
|
||||
// ★ PHP→Java变更轮询(管理员在PHP改了配置/领地,Java及时拉取)
|
||||
if (initialSyncComplete) {
|
||||
submitNormalDbTask("TimerA-pollAdminChanges", () -> {
|
||||
try { pollAdminChanges(); } catch (Exception e) { /* 静默 */ }
|
||||
});
|
||||
if (!initialSyncComplete) return;
|
||||
|
||||
// 远程CDK(cdk_validate_requests,sdf1计分板CDK验证)
|
||||
try {
|
||||
pullWebCdkRequestsAndValidate();
|
||||
pullSdf1PendingAndValidateWeb();
|
||||
} catch (Exception e) { /* 静默 */ }
|
||||
|
||||
// ★ PHP→Java变更轮询(管理员在PHP改了配置/领地,Java及时拉取)
|
||||
try { pollAdminChanges(); } catch (Exception e) { /* 静默 */ }
|
||||
// ★ 过户cooldown检测:权限变更则取消过户
|
||||
submitNormalDbTask("TimerA-transferCancellations", () -> {
|
||||
try { handlePendingTransferCancellations(); } catch (Exception e) { /* 静默 */ }
|
||||
});
|
||||
}
|
||||
// ★ 异步玩家验证轮询:拉取PHP的pending_player_validations,验证后推回结果
|
||||
if (initialSyncComplete) {
|
||||
submitNormalDbTask("TimerA-pollPlayerValidations", () -> {
|
||||
try { pullPendingPlayerValidations(); } catch (Exception e) { /* 静默 */ }
|
||||
});
|
||||
}
|
||||
try { handlePendingTransferCancellations(); } catch (Exception e) { /* 静默 */ }
|
||||
// ★ 异步玩家验证轮询:拉取PHP的pending_player_validations,验证后推回结果
|
||||
try { pullPendingPlayerValidations(); } catch (Exception e) { /* 静默 */ }
|
||||
});
|
||||
}
|
||||
|
||||
// 自调度下一轮(3~5秒,快速响应登录请求,错峰避免锁库)
|
||||
@@ -3383,6 +3411,8 @@ public class WebManager {
|
||||
hashBuilder.append(land.getOrDefault("deny_crop_harvest", 0)).append(":");
|
||||
hashBuilder.append(land.getOrDefault("deny_wool_shear", 0)).append(":");
|
||||
hashBuilder.append(land.getOrDefault("deny_animal_feeding", 0)).append(":");
|
||||
hashBuilder.append(land.getOrDefault("deny_spawn_egg", 0)).append(":");
|
||||
hashBuilder.append(land.getOrDefault("deny_wax", 0)).append(":");
|
||||
hashBuilder.append(land.getOrDefault("deny_mob_attack", 0)).append(":");
|
||||
hashBuilder.append(land.getOrDefault("deny_fire", 0)).append(":");
|
||||
hashBuilder.append(land.getOrDefault("deny_all_effects", 0)).append(":");
|
||||
@@ -3720,12 +3750,31 @@ public class WebManager {
|
||||
}
|
||||
lastBansHash = currentHash;
|
||||
|
||||
// ★ 封禁IP → 顺手推给Web黑名单(web_ip_blacklist)
|
||||
// Web端据此拦截被封IP的访问,除非该IP持有游戏内 /web 签发的token
|
||||
StringBuilder ipsb = new StringBuilder("[");
|
||||
boolean ipFirst = true;
|
||||
@SuppressWarnings("unchecked")
|
||||
Set<org.bukkit.BanEntry<?>> ipOnlyEntries = (Set<org.bukkit.BanEntry<?>>)(Set<?>) ipBanList.getEntries();
|
||||
for (org.bukkit.BanEntry<?> entry : ipOnlyEntries) {
|
||||
if (!ipFirst) ipsb.append(",");
|
||||
ipFirst = false;
|
||||
long expireDate = entry.getExpiration() != null ? entry.getExpiration().getTime() : 0;
|
||||
ipsb.append("{\"ip\":\"").append(escapeJson(entry.getTarget()))
|
||||
.append("\",\"reason\":\"").append(escapeJson(entry.getReason() != null ? entry.getReason() : ""))
|
||||
.append("\",\"source\":\"").append(escapeJson(entry.getSource() != null ? entry.getSource() : ""))
|
||||
.append("\",\"expire\":").append(expireDate).append("}");
|
||||
}
|
||||
ipsb.append("]");
|
||||
|
||||
// ★ 改用POST避免GET URL过长(55条封禁URL可达数KB)
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("secret", secretKey);
|
||||
body.put("bans", sb.toString());
|
||||
body.put("ips", ipsb.toString());
|
||||
String resp = httpPost("api/sync.php?action=sync_bans", mapToJson(body));
|
||||
plugin.getLogger().info("[防护-sync] 封禁名单同步: " + (nameEntries.size() + ipEntries.size()) + "条 → " + resp);
|
||||
plugin.getLogger().info("[防护-sync] 封禁名单同步: " + (nameEntries.size() + ipEntries.size())
|
||||
+ "条(IP黑名单 " + ipOnlyEntries.size() + "个) → " + resp);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[防护-sync] 封禁名单同步异常: " + e.getMessage());
|
||||
}
|
||||
@@ -5166,6 +5215,8 @@ public class WebManager {
|
||||
}
|
||||
|
||||
final String[] pollData = {sessionId};
|
||||
// ★ 连续无效响应(非JSON/结构不符)计数:只用于日志与提示,任何情况下都不会据此放行
|
||||
final int[] invalidPolls = {0};
|
||||
final long startTime = System.currentTimeMillis();
|
||||
final long maxPollTime = 660000; // 11分钟(比会话过期多1分钟)
|
||||
|
||||
@@ -5213,16 +5264,69 @@ public class WebManager {
|
||||
String response = doGet(url);
|
||||
if (response == null) return;
|
||||
|
||||
com.google.gson.JsonObject json = com.google.gson.JsonParser.parseString(response).getAsJsonObject();
|
||||
if (!json.get("success").getAsBoolean()) return;
|
||||
|
||||
// ★ fail-closed:非JSON / 结构不符 一律视为"无效响应",绝不判为通过
|
||||
com.google.gson.JsonObject json;
|
||||
try {
|
||||
json = com.google.gson.JsonParser.parseString(response).getAsJsonObject();
|
||||
} catch (Exception parseEx) {
|
||||
invalidPolls[0]++;
|
||||
if (invalidPolls[0] == 1 || invalidPolls[0] % 15 == 0) {
|
||||
String preview = response.length() > 200 ? response.substring(0, 200) + "..." : response;
|
||||
plugin.getLogger().warning("[正版验证] 响应非JSON,忽略(第" + invalidPolls[0] + "次): " + preview);
|
||||
}
|
||||
if (invalidPolls[0] == 5) {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
if (player.isOnline()) {
|
||||
player.sendMessage("§c验证服务响应异常,正在自动重试…");
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!json.has("success") || !json.get("success").isJsonPrimitive()
|
||||
|| !json.get("success").getAsBoolean()) {
|
||||
// PHP明确返回失败(会话不存在/密钥错误等结构性错误)→ 终止轮询,不放行
|
||||
String err = (json.has("error") && json.get("error").isJsonPrimitive())
|
||||
? json.get("error").getAsString() : "未知错误";
|
||||
plugin.getLogger().warning("[正版验证] PHP返回失败,终止轮询: " + err);
|
||||
minecraftAuthSessions.remove(playerName);
|
||||
minecraftAuthPollers.remove(playerName);
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
if (player.isOnline()) {
|
||||
player.sendMessage("§c验证失败: " + err);
|
||||
}
|
||||
});
|
||||
this.cancel();
|
||||
return;
|
||||
}
|
||||
if (!json.has("data") || !json.get("data").isJsonObject()) {
|
||||
invalidPolls[0]++;
|
||||
plugin.getLogger().warning("[正版验证] 响应缺少data,忽略(第" + invalidPolls[0] + "次)");
|
||||
return;
|
||||
}
|
||||
com.google.gson.JsonObject data = json.getAsJsonObject("data");
|
||||
if (!data.has("status") || !data.get("status").isJsonPrimitive()) {
|
||||
invalidPolls[0]++;
|
||||
plugin.getLogger().warning("[正版验证] 响应缺少status,忽略(第" + invalidPolls[0] + "次)");
|
||||
return;
|
||||
}
|
||||
String status = data.get("status").getAsString();
|
||||
invalidPolls[0] = 0; // 收到有效响应 → 重置无效计数
|
||||
|
||||
if ("verified".equals(status)) {
|
||||
// 验证成功!
|
||||
String mcUuid = data.get("mc_uuid").getAsString();
|
||||
String mcUsername = data.get("mc_username").getAsString();
|
||||
String mcUuid = data.has("mc_uuid") ? data.get("mc_uuid").getAsString() : "";
|
||||
String mcUsername = data.has("mc_username") ? data.get("mc_username").getAsString() : "";
|
||||
String sessionPlayer = data.has("player_name") ? data.get("player_name").getAsString() : "";
|
||||
|
||||
// ★ fail-closed:凭证不全 / 会话归属玩家不符 → 一律不放行(继续轮询直到超时)
|
||||
if (mcUuid.isEmpty() || mcUsername.isEmpty()
|
||||
|| (sessionPlayer != null && !sessionPlayer.isEmpty()
|
||||
&& !sessionPlayer.equalsIgnoreCase(playerName))) {
|
||||
plugin.getLogger().warning("[正版验证] verified但校验不通过,拒绝放行: session.player="
|
||||
+ sessionPlayer + ", uuid=" + mcUuid + ", name=" + mcUsername);
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getLogger().info("[正版验证] 验证成功: " + playerName + " -> " + mcUsername + " (" + mcUuid + ")");
|
||||
|
||||
@@ -6326,7 +6430,9 @@ public class WebManager {
|
||||
if (arrStr.trim().equals("[]")) {
|
||||
return;
|
||||
}
|
||||
plugin.getLogger().info("[Web密码验证轮询] ★ 发现待处理请求,数组长度=" + arrStr.length());
|
||||
int pendingCount = countTopLevelJsonObjects(arrStr);
|
||||
plugin.getLogger().info("[Web密码验证轮询] ★ 发现待处理请求,条数=" + pendingCount
|
||||
+ "(JSON长度=" + arrStr.length() + ")");
|
||||
int idx = 0;
|
||||
while (true) {
|
||||
int objStart = arrStr.indexOf("{", idx);
|
||||
@@ -6374,7 +6480,15 @@ public class WebManager {
|
||||
}
|
||||
|
||||
// 异步将结果写回PHP(供Web端查询)
|
||||
sendWebLoginResult(fReqId, fName, result);
|
||||
try {
|
||||
sendWebLoginResult(fReqId, fName, result);
|
||||
} catch (Throwable t) {
|
||||
// ★ 热替换jar后类加载器可能找不到匿名/内部类(Error不是Exception,
|
||||
// 外层catch接不住)→ 复位处理标记,下一轮重新验证重试
|
||||
plugin.getLogger().warning("[Web密码验证] 回写调度失败: " + t
|
||||
+ " → 已复位处理标记,稍后重试");
|
||||
processedWebLoginRequests.remove(fReqId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[Web密码验证] 处理异常: player=" + fName + " error=" + e.getMessage());
|
||||
}
|
||||
@@ -6392,62 +6506,67 @@ public class WebManager {
|
||||
/**
|
||||
* 将密码验证结果写回PHP后端
|
||||
* 使用GET请求传所有参数(避免POST body解析问题),同时增加重试机制
|
||||
*
|
||||
* ★ 用 lambda 而非匿名内部类(原 WebManager$15):
|
||||
* jar 被热替换后 PluginClassLoader 找不到旧的匿名内部类,
|
||||
* 抛 NoClassDefFoundError → 回写永远失败 → PHP pending 永不清空。
|
||||
* lambda 编译为 WebManager 自身的合成方法,类已加载,不受热替换影响。
|
||||
*/
|
||||
private void sendWebLoginResult(String reqId, String playerName, String result) {
|
||||
new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// 将result转换为JSON对象格式 {"success": true/false, "status": "..."}
|
||||
String resultJson;
|
||||
if ("\"success\"".equals(result)) {
|
||||
resultJson = "{\"success\":true,\"status\":\"success\"}";
|
||||
} else if ("\"not_registered\"".equals(result)) {
|
||||
resultJson = "{\"success\":false,\"status\":\"not_registered\"}";
|
||||
} else {
|
||||
resultJson = "{\"success\":false,\"status\":\"failed\"}";
|
||||
}
|
||||
|
||||
plugin.getLogger().info("[Web密码验证回写] ★ 开始回写PHP player=" + playerName
|
||||
+ " reqId=" + reqId + " result=" + result);
|
||||
|
||||
// 使用GET请求,所有参数通过URL传递,避免POST body解析问题
|
||||
// 最多重试3次,每次间隔2秒
|
||||
boolean sent = false;
|
||||
for (int attempt = 0; attempt < 3; attempt++) {
|
||||
try {
|
||||
String resultEncoded = java.net.URLEncoder.encode(resultJson, "UTF-8");
|
||||
String urlStr = webBaseUrl + "/api/sync.php?action=complete_web_login_request"
|
||||
+ "&secret=" + java.net.URLEncoder.encode(secretKey, "UTF-8")
|
||||
+ "&request_id=" + reqId
|
||||
+ "&player=" + java.net.URLEncoder.encode(playerName, "UTF-8")
|
||||
+ "&result=" + resultEncoded;
|
||||
|
||||
String resp = doGet(urlStr);
|
||||
|
||||
if (resp != null) {
|
||||
sent = true;
|
||||
plugin.getLogger().info("[Web密码验证回写] ✓ 成功: player=" + playerName
|
||||
+ " reqId=" + reqId + " result=" + result
|
||||
+ " PHP响应=" + resp.substring(0, Math.min(200, resp.length()))
|
||||
+ " (第" + (attempt + 1) + "次)");
|
||||
break;
|
||||
} else {
|
||||
plugin.getLogger().warning("[Web密码验证回写] ✗ GET失败: player=" + playerName
|
||||
+ " reqId=" + reqId + " (第" + (attempt + 1) + "次)");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[Web密码验证回写] ✗ 异常: player=" + playerName
|
||||
+ " reqId=" + reqId + " (第" + (attempt + 1) + "次) " + e.getMessage());
|
||||
}
|
||||
// 重试前等待
|
||||
try { Thread.sleep(2000); } catch (InterruptedException ie) { break; }
|
||||
}
|
||||
|
||||
if (!sent) {
|
||||
plugin.getLogger().warning("[Web密码验证回写] ✗ 最终失败,已重试3次: player=" + playerName + " reqId=" + reqId);
|
||||
}
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
// 将result转换为JSON对象格式 {"success": true/false, "status": "..."}
|
||||
String resultJson;
|
||||
if ("\"success\"".equals(result)) {
|
||||
resultJson = "{\"success\":true,\"status\":\"success\"}";
|
||||
} else if ("\"not_registered\"".equals(result)) {
|
||||
resultJson = "{\"success\":false,\"status\":\"not_registered\"}";
|
||||
} else {
|
||||
resultJson = "{\"success\":false,\"status\":\"failed\"}";
|
||||
}
|
||||
}.runTaskAsynchronously(plugin);
|
||||
|
||||
plugin.getLogger().info("[Web密码验证回写] ★ 开始回写PHP player=" + playerName
|
||||
+ " reqId=" + reqId + " result=" + result);
|
||||
|
||||
// 使用GET请求,所有参数通过URL传递,避免POST body解析问题
|
||||
// 最多重试3次,每次间隔2秒
|
||||
boolean sent = false;
|
||||
for (int attempt = 0; attempt < 3; attempt++) {
|
||||
try {
|
||||
String resultEncoded = java.net.URLEncoder.encode(resultJson, "UTF-8");
|
||||
String urlStr = webBaseUrl + "/api/sync.php?action=complete_web_login_request"
|
||||
+ "&secret=" + java.net.URLEncoder.encode(secretKey, "UTF-8")
|
||||
+ "&request_id=" + reqId
|
||||
+ "&player=" + java.net.URLEncoder.encode(playerName, "UTF-8")
|
||||
+ "&result=" + resultEncoded;
|
||||
|
||||
String resp = doGet(urlStr);
|
||||
|
||||
if (resp != null) {
|
||||
sent = true;
|
||||
plugin.getLogger().info("[Web密码验证回写] ✓ 成功: player=" + playerName
|
||||
+ " reqId=" + reqId + " result=" + result
|
||||
+ " PHP响应=" + resp.substring(0, Math.min(200, resp.length()))
|
||||
+ " (第" + (attempt + 1) + "次)");
|
||||
break;
|
||||
} else {
|
||||
plugin.getLogger().warning("[Web密码验证回写] ✗ GET失败: player=" + playerName
|
||||
+ " reqId=" + reqId + " (第" + (attempt + 1) + "次)");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[Web密码验证回写] ✗ 异常: player=" + playerName
|
||||
+ " reqId=" + reqId + " (第" + (attempt + 1) + "次) " + e.getMessage());
|
||||
}
|
||||
// 重试前等待
|
||||
try { Thread.sleep(2000); } catch (InterruptedException ie) { break; }
|
||||
}
|
||||
|
||||
if (!sent) {
|
||||
plugin.getLogger().warning("[Web密码验证回写] ✗ 最终失败,已重试3次: player=" + playerName + " reqId=" + reqId);
|
||||
// ★ 回写失败 → 复位"已处理"标记,下一轮轮询重新验证并重试写回,
|
||||
// 否则该请求会在PHP里挂满10分钟过期,Web端一直转圈
|
||||
processedWebLoginRequests.remove(reqId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -6489,6 +6608,28 @@ public class WebManager {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计JSON数组中顶层对象 {…} 的个数(用于日志显示真实待处理条数,
|
||||
* 以前打印的是字符串长度,容易被误读成条数)
|
||||
*/
|
||||
private static int countTopLevelJsonObjects(String arr) {
|
||||
if (arr == null || arr.isEmpty()) return 0;
|
||||
int count = 0, depth = 0;
|
||||
boolean inString = false;
|
||||
for (int i = 0; i < arr.length(); i++) {
|
||||
char c = arr.charAt(i);
|
||||
if (inString) {
|
||||
if (c == '\\' && i + 1 < arr.length()) { i++; continue; }
|
||||
if (c == '"') inString = false;
|
||||
continue;
|
||||
}
|
||||
if (c == '"') { inString = true; continue; }
|
||||
if (c == '{') { if (depth == 0) count++; depth++; }
|
||||
else if (c == '}') { if (depth > 0) depth--; }
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// ==================== 新增功能 ====================
|
||||
|
||||
// 日志控制:避免频繁打印
|
||||
|
||||
Reference in New Issue
Block a user