Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
168d4f39dc | ||
|
|
862a2736b3 | ||
|
|
aaffc760f3 | ||
|
|
8ce4d58195 | ||
|
|
6149794e36 | ||
|
|
0d3547dded | ||
|
|
a1e1e919e1 | ||
|
|
a145a90f41 | ||
|
|
b8ccd56818 | ||
|
|
89525a4e6b | ||
|
|
8b8823e01d | ||
|
|
38d06c3cfa | ||
|
|
d20a525855 | ||
|
|
faa17ded32 | ||
|
|
e8588ae408 | ||
|
|
3f7c8e4001 | ||
|
|
4692b32a6b | ||
|
|
5d3b2c81c9 | ||
|
|
0ec3bc46dd | ||
|
|
53c805c68f | ||
|
|
34aca10edb | ||
|
|
cdce0c0e19 | ||
|
|
b488dfa04f | ||
|
|
9213918995 | ||
|
|
4ae8b595e7 | ||
|
|
23ab0c3457 | ||
|
|
fc72e231d3 | ||
|
|
c64bc718a6 | ||
|
|
e860b6ff4f | ||
|
|
a6d70aed3c | ||
|
|
2fc34375d3 | ||
|
|
9958f09fcd | ||
|
|
8523e52919 | ||
|
|
b1379350b5 | ||
|
|
e0644a0ec4 | ||
|
|
e4640d9102 | ||
|
|
09d0be84b3 | ||
|
|
bc46ec27d0 | ||
|
|
7ab5aee87c | ||
|
|
f19efa89a8 | ||
|
|
182545de59 | ||
|
|
c4a5fc9a61 | ||
|
|
caaf09fce0 |
@@ -32,25 +32,69 @@ jobs:
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
TAG="build-${{ gitea.run_number }}"
|
||||
set -e
|
||||
# 从 plugin.yml 提取版本号作为标签
|
||||
VERSION=$(grep '^version:' src/main/resources/plugin.yml | awk '{print $2}' | tr -d '\r\n ')
|
||||
TAG="v${VERSION}"
|
||||
BASE="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
|
||||
echo "Version: ${VERSION}, Tag: ${TAG}"
|
||||
echo "Token prefix: ${GITEA_TOKEN:0:8}..."
|
||||
|
||||
curl -s -X POST \
|
||||
# 测试 token 权限
|
||||
echo "=== Testing API access ==="
|
||||
curl -s -w "\nHTTP_CODE:%{http_code}\n" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
"${BASE}" | tail -5
|
||||
|
||||
# 如果同版本标签已存在,先删除旧 release
|
||||
echo "=== Checking existing tag ${TAG} ==="
|
||||
RESP=$(curl -s -w "\nHTTP_CODE:%{http_code}" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
"${BASE}/releases/tags/${TAG}")
|
||||
HTTP_CODE=$(echo "$RESP" | grep -o 'HTTP_CODE:[0-9]*' | cut -d: -f2)
|
||||
echo "Check tag HTTP: ${HTTP_CODE}"
|
||||
|
||||
if [ "$HTTP_CODE" = "200" ]; then
|
||||
OLD_ID=$(echo "$RESP" | head -1 | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
|
||||
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
|
||||
elif [ "$HTTP_CODE" = "404" ]; then
|
||||
echo "Tag ${TAG} not found, will create new"
|
||||
else
|
||||
echo "Warning: unexpected response checking tag"
|
||||
fi
|
||||
|
||||
# 创建新 release
|
||||
echo "=== Creating release ${TAG} ==="
|
||||
RELEASE_RESP=$(curl -s -w "\nHTTP_CODE:%{http_code}" -X POST \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\":\"${TAG}\",\"name\":\"Build ${{ gitea.run_number }}\",\"body\":\"Commit: ${{ gitea.sha }}\nBranch: ${{ gitea.ref_name }}\",\"draft\":false,\"prerelease\":false}" \
|
||||
"${BASE}/releases"
|
||||
-d "{\"tag_name\":\"${TAG}\",\"name\":\"Sdf1_login ${TAG}\",\"body\":\"Version: ${VERSION}\\nCommit: ${{ gitea.sha }}\\nBranch: ${{ gitea.ref_name }}\",\"draft\":false,\"prerelease\":false}" \
|
||||
"${BASE}/releases")
|
||||
RELEASE_HTTP=$(echo "$RELEASE_RESP" | grep -o 'HTTP_CODE:[0-9]*' | cut -d: -f2)
|
||||
echo "Create release HTTP: ${RELEASE_HTTP}"
|
||||
|
||||
RELEASE_ID=$(curl -s \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
"${BASE}/releases/tags/${TAG}" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
|
||||
if [ "$RELEASE_HTTP" != "201" ]; then
|
||||
echo "=== Release creation failed! ==="
|
||||
echo "$RELEASE_RESP" | head -1
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 上传 JAR 资产
|
||||
RELEASE_ID=$(echo "$RELEASE_RESP" | head -1 | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
|
||||
if [ -n "$RELEASE_ID" ]; then
|
||||
echo "=== Uploading JAR to release #${RELEASE_ID} ==="
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-F "attachment=@target/Sdf1_login.jar" \
|
||||
"${BASE}/releases/${RELEASE_ID}/assets"
|
||||
echo "Release ${TAG} created with JAR"
|
||||
echo "✅ Release ${TAG} created with JAR"
|
||||
else
|
||||
echo "Release tag ${TAG} created (JAR attach may need manual check)"
|
||||
echo "⚠️ Release created but could not get release ID for JAR upload"
|
||||
fi
|
||||
|
||||
@@ -46,6 +46,9 @@ backup/
|
||||
新建*.txt
|
||||
群戏归类.txt
|
||||
upload_sftp.py
|
||||
deploy_*.py
|
||||
deploy_*.ps1
|
||||
deploy_*.sh
|
||||
|
||||
# PHP Web 后端
|
||||
web/Web.db
|
||||
@@ -58,6 +61,11 @@ web/self_update.php
|
||||
web/version_check.php
|
||||
web/debug_*.php
|
||||
web/db/debug_*.php
|
||||
# 支付密钥 - 严禁提交 (double insurance,配合 Web/.gitignore)
|
||||
Web/密钥文件_git拉黑.txt
|
||||
web/密钥文件_git拉黑.txt
|
||||
Web/pay_secrets.php
|
||||
web/pay_secrets.php
|
||||
|
||||
# Windows 保留设备名(无法用普通 rm 删除)
|
||||
nul
|
||||
|
||||
@@ -565,6 +565,8 @@ public class AreaCLIManager {
|
||||
case "audio": oldState = land.denyNoteblockJukebox; land.denyNoteblockJukebox = !land.denyNoteblockJukebox; break;
|
||||
case "lead": oldState = land.denyLead; land.denyLead = !land.denyLead; break;
|
||||
case "crop_harvest": oldState = land.denyCropHarvest; land.denyCropHarvest = !land.denyCropHarvest; break;
|
||||
case "farmland_trample": oldState = land.denyFarmlandTrample; land.denyFarmlandTrample = !land.denyFarmlandTrample; break;
|
||||
case "ender_teleport": oldState = land.denyEnderTeleport; land.denyEnderTeleport = !land.denyEnderTeleport; break;
|
||||
case "wool_shear": oldState = land.denyWoolShear; land.denyWoolShear = !land.denyWoolShear; break;
|
||||
case "animal_feed": oldState = land.denyAnimalFeeding; land.denyAnimalFeeding = !land.denyAnimalFeeding; break;
|
||||
case "mob_attack": oldState = land.denyMobAttack; land.denyMobAttack = !land.denyMobAttack; break;
|
||||
@@ -622,6 +624,8 @@ public class AreaCLIManager {
|
||||
case "audio": return "denyNoteblockJukebox";
|
||||
case "lead": return "denyLead";
|
||||
case "crop_harvest": return "denyCropHarvest";
|
||||
case "farmland_trample": return "denyFarmlandTrample";
|
||||
case "ender_teleport": return "denyEnderTeleport";
|
||||
case "wool_shear": return "denyWoolShear";
|
||||
case "animal_feed": return "denyAnimalFeeding";
|
||||
case "mob_attack": return "denyMobAttack";
|
||||
@@ -682,6 +686,8 @@ public class AreaCLIManager {
|
||||
perms.add(new PermItem("audio", "音频(音符盒/唱片机)", !land.denyNoteblockJukebox));
|
||||
perms.add(new PermItem("lead", "拴绳使用", !land.denyLead));
|
||||
perms.add(new PermItem("crop_harvest", land.denyCropHarvest ? "禁用农作物收获" : "启用农作物收获", !land.denyCropHarvest));
|
||||
perms.add(new PermItem("farmland_trample", land.denyFarmlandTrample ? "禁用耕地破坏" : "启用耕地破坏", !land.denyFarmlandTrample));
|
||||
perms.add(new PermItem("ender_teleport", land.denyEnderTeleport ? "禁用末影人行为" : "启用末影人行为", !land.denyEnderTeleport));
|
||||
perms.add(new PermItem("wool_shear", "剪切羊毛/生物", !land.denyWoolShear));
|
||||
perms.add(new PermItem("animal_feed", land.denyAnimalFeeding ? "禁止投喂" : "允许投喂动物", !land.denyAnimalFeeding));
|
||||
perms.add(new PermItem("mob_attack", land.denyMobAttack ? "禁止攻击生物" : "允许攻击生物", !land.denyMobAttack));
|
||||
@@ -721,6 +727,8 @@ public class AreaCLIManager {
|
||||
case "audio": return "音频";
|
||||
case "lead": return "拴绳使用";
|
||||
case "crop_harvest": return "农作物收获";
|
||||
case "farmland_trample": return "耕地破坏";
|
||||
case "ender_teleport": return "末影人传送";
|
||||
case "wool_shear": return "剪切羊毛";
|
||||
case "animal_feed": return "投喂动物";
|
||||
case "mob_attack": return "攻击生物";
|
||||
@@ -1135,6 +1143,8 @@ public class AreaCLIManager {
|
||||
{"audio", "音频(音符盒/唱片机)", "denyNoteblockJukebox"},
|
||||
{"lead", "拴绳使用", "denyLead"},
|
||||
{"crop_harvest", "农作物收获", "denyCropHarvest"},
|
||||
{"farmland_trample", "耕地破坏", "denyFarmlandTrample"},
|
||||
{"ender_teleport", "末影人行为", "denyEnderTeleport"},
|
||||
{"wool_shear", "剪切羊毛/生物", "denyWoolShear"},
|
||||
{"animal_feed", "投喂动物", "denyAnimalFeeding"},
|
||||
{"mob_attack", "攻击生物", "denyMobAttack"},
|
||||
@@ -1173,6 +1183,14 @@ public class AreaCLIManager {
|
||||
if (field.equals("denySignEdit")) {
|
||||
name = landDefault ? "禁止编辑告示牌" : "允许编辑告示牌";
|
||||
}
|
||||
// ★ denyFarmlandTrample显示名称
|
||||
if (field.equals("denyFarmlandTrample")) {
|
||||
name = landDefault ? "禁用耕地破坏" : "启用耕地破坏";
|
||||
}
|
||||
// ★ denyEnderTeleport显示名称(末影人行为)
|
||||
if (field.equals("denyEnderTeleport")) {
|
||||
name = landDefault ? "禁用末影人行为" : "启用末影人行为";
|
||||
}
|
||||
|
||||
// 如果有per-player覆盖,使用覆盖值;否则使用领地默认
|
||||
if (playerPerms.containsKey(field)) {
|
||||
@@ -1220,6 +1238,8 @@ public class AreaCLIManager {
|
||||
case "denyNoteblockJukebox": return land.denyNoteblockJukebox;
|
||||
case "denyLead": return land.denyLead;
|
||||
case "denyCropHarvest": return land.denyCropHarvest;
|
||||
case "denyFarmlandTrample": return land.denyFarmlandTrample;
|
||||
case "denyEnderTeleport": return land.denyEnderTeleport;
|
||||
case "denyWoolShear": return land.denyWoolShear;
|
||||
case "denyAnimalFeeding": return land.denyAnimalFeeding;
|
||||
case "denyGlowing": return land.denyGlowing;
|
||||
|
||||
@@ -498,7 +498,8 @@ public class AreaGUIManager implements Listener {
|
||||
{"农作物收获", "denyCropHarvest"}, {"剪切羊毛", "denyWoolShear"}, {"投喂动物", "denyAnimalFeeding"},
|
||||
{"攻击生物", "denyMobAttack"},
|
||||
{"禁止编辑告示牌", "denySignEdit"},
|
||||
{"玩家发光", "denyGlowing"}, {"传送", "allowTeleport"}, {"和平模式", "peaceMode"}
|
||||
{"玩家发光", "denyGlowing"}, {"传送", "allowTeleport"}, {"和平模式", "peaceMode"},
|
||||
{"耕地破坏", "denyFarmlandTrample"}, {"末影人行为", "denyEnderTeleport"}
|
||||
};
|
||||
|
||||
for (String[] def : permDefs) {
|
||||
@@ -529,6 +530,12 @@ public class AreaGUIManager implements Listener {
|
||||
if (field.equals("denySignEdit")) {
|
||||
name = landDefault ? "禁止编辑告示牌" : "允许编辑告示牌";
|
||||
}
|
||||
if (field.equals("denyFarmlandTrample")) {
|
||||
name = landDefault ? "禁用耕地破坏" : "启用耕地破坏";
|
||||
}
|
||||
if (field.equals("denyEnderTeleport")) {
|
||||
name = landDefault ? "禁用末影人行为" : "启用末影人行为";
|
||||
}
|
||||
|
||||
// ★ 对于 allowPickup/allowDrop,enabled=true 表示"允许操作"(即 allow=true)
|
||||
// 其他权限,enabled = true 表示"允许操作"(即 deny=false)
|
||||
@@ -691,6 +698,8 @@ public class AreaGUIManager implements Listener {
|
||||
perms.add(new PermEntry(land.denyMobAttack ? "禁止攻击生物" : "允许攻击生物", !land.denyMobAttack));
|
||||
perms.add(new PermEntry(land.denySignEdit ? "禁止编辑告示牌" : "允许编辑告示牌", !land.denySignEdit));
|
||||
perms.add(new PermEntry("玩家发光", !land.denyGlowing));
|
||||
perms.add(new PermEntry(land.denyFarmlandTrample ? "禁用耕地破坏" : "启用耕地破坏", !land.denyFarmlandTrample));
|
||||
perms.add(new PermEntry(land.denyEnderTeleport ? "禁用末影人行为" : "启用末影人行为", !land.denyEnderTeleport));
|
||||
perms.add(new PermEntry("传送", land.allowVisitorTeleport));
|
||||
perms.add(new PermEntry("和平模式", land.peaceMode));
|
||||
perms.add(new PermEntry("公共建筑设施", land.isPublicBuilding));
|
||||
@@ -752,6 +761,8 @@ public class AreaGUIManager implements Listener {
|
||||
case "禁止攻击生物": case "允许攻击生物": land.denyMobAttack = !land.denyMobAttack; break;
|
||||
case "禁止编辑告示牌": case "允许编辑告示牌": land.denySignEdit = !land.denySignEdit; break;
|
||||
case "玩家发光": land.denyGlowing = !land.denyGlowing; break;
|
||||
case "禁用耕地破坏": case "启用耕地破坏": land.denyFarmlandTrample = !land.denyFarmlandTrample; break;
|
||||
case "禁用末影人行为": case "启用末影人行为": land.denyEnderTeleport = !land.denyEnderTeleport; break;
|
||||
case "传送": land.allowVisitorTeleport = !land.allowVisitorTeleport; break;
|
||||
case "和平模式": land.peaceMode = !land.peaceMode; break;
|
||||
case "公共建筑设施": land.isPublicBuilding = !land.isPublicBuilding; break;
|
||||
|
||||
@@ -46,6 +46,7 @@ import java.util.HashSet;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
import org.bukkit.event.entity.EntitySpawnEvent;
|
||||
import com.destroystokyo.paper.event.entity.EntityPathfindEvent;
|
||||
import org.bukkit.entity.Item;
|
||||
import org.bukkit.event.inventory.InventoryOpenEvent;
|
||||
|
||||
@@ -322,18 +323,19 @@ public class AreaProtection implements Listener {
|
||||
private boolean isExemptFromGameMode(
|
||||
Player p, String areaName) {
|
||||
String name = p.getName();
|
||||
String lower = name.toLowerCase();
|
||||
|
||||
// 管理员豁免
|
||||
if (isAreaAdmin(p)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (globalPlayerWhitelist.contains(name)) {
|
||||
if (globalPlayerWhitelist.contains(lower)) {
|
||||
return true;
|
||||
}
|
||||
Set<String> aw =
|
||||
areaPlayerWhitelist.get(areaName);
|
||||
if (aw != null && aw.contains(name)) {
|
||||
if (aw != null && aw.contains(lower)) {
|
||||
return true;
|
||||
}
|
||||
AreaConfig ac = areas.get(areaName);
|
||||
@@ -1220,6 +1222,8 @@ public class AreaProtection implements Listener {
|
||||
try { ac.denyNoteblockJukebox = rs.getInt("deny_noteblock_jukebox") == 1; } catch (Exception ignored) {}
|
||||
try { ac.denyLead = rs.getInt("deny_lead") == 1; } catch (Exception ignored) {}
|
||||
try { ac.denyCropHarvest = rs.getInt("deny_crop_harvest") == 1; } catch (Exception ignored) {}
|
||||
try { ac.denyFarmlandTrample = rs.getInt("deny_farmland_trample") == 1; } catch (Exception ignored) {}
|
||||
try { ac.denyEnderTeleport = rs.getInt("deny_ender_teleport") == 1; } catch (Exception ignored) {}
|
||||
try { ac.denyWoolShear = rs.getInt("deny_wool_shear") == 1; } catch (Exception ignored) {}
|
||||
try { ac.denyAnimalFeeding = rs.getInt("deny_animal_feeding") == 1; } catch (Exception ignored) {}
|
||||
try { ac.denyContainer = rs.getInt("deny_container") == 1; } catch (Exception ignored) {}
|
||||
@@ -1496,6 +1500,8 @@ public class AreaProtection implements Listener {
|
||||
try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_noteblock_jukebox INTEGER DEFAULT 0"); } catch (Exception ignored) {}
|
||||
try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_lead INTEGER DEFAULT 0"); } catch (Exception ignored) {}
|
||||
try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_crop_harvest INTEGER DEFAULT 0"); } catch (Exception ignored) {}
|
||||
try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_farmland_trample INTEGER DEFAULT 0"); } catch (Exception ignored) {}
|
||||
try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_ender_teleport INTEGER DEFAULT 0"); } catch (Exception ignored) {}
|
||||
try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_wool_shear INTEGER DEFAULT 0"); } catch (Exception ignored) {}
|
||||
try { stmt.executeUpdate("ALTER TABLE area_lands ADD COLUMN deny_animal_feeding INTEGER DEFAULT 0"); } catch (Exception ignored) {}
|
||||
// ★ 传送点列
|
||||
@@ -2882,6 +2888,8 @@ public class AreaProtection implements Listener {
|
||||
case "denyNoteblockJukebox": return ac.denyNoteblockJukebox;
|
||||
case "denyLead": return ac.denyLead;
|
||||
case "denyCropHarvest": return ac.denyCropHarvest;
|
||||
case "denyFarmlandTrample": return ac.denyFarmlandTrample;
|
||||
case "denyEnderTeleport": return ac.denyEnderTeleport;
|
||||
case "denyWoolShear": return ac.denyWoolShear;
|
||||
case "denyAnimalFeeding": return ac.denyAnimalFeeding;
|
||||
case "denyMobAttack": return ac.denyMobAttack;
|
||||
@@ -3230,6 +3238,8 @@ public class AreaProtection implements Listener {
|
||||
AreaConfig ac = areas.get(newArea);
|
||||
if (ac != null) {
|
||||
// ★ denyMove检查:支持per-player独立权限
|
||||
// getEffectiveDeny 已内置 ADMIN/OWNER/VISITOR 豁免,
|
||||
// 对无权限玩家则走领地默认 denyMove 配置
|
||||
if (getEffectiveDeny(p, ac, "denyMove")) {
|
||||
Location safeLoc = findSafeExitLocation(p.getLocation(), ac);
|
||||
if (safeLoc != null) {
|
||||
@@ -3390,6 +3400,7 @@ public class AreaProtection implements Listener {
|
||||
AreaConfig newAc = areas.get(newArea);
|
||||
if (newAc != null) {
|
||||
// ★ denyMove检查:支持per-player独立权限
|
||||
// getEffectiveDeny 已内置 ADMIN/OWNER/VISITOR 豁免
|
||||
if (getEffectiveDeny(p, newAc, "denyMove")) {
|
||||
Location safeLoc = findSafeExitLocation(event.getFrom(), newAc);
|
||||
if (safeLoc != null) {
|
||||
@@ -5342,6 +5353,14 @@ public class AreaProtection implements Listener {
|
||||
e.getBlock().getX(), e.getBlock().getY(), e.getBlock().getZ());
|
||||
if (ac != null && ac.denyFire) e.setCancelled(true);
|
||||
}
|
||||
// ★ 末影人行为控制:禁止末影人抱走方块
|
||||
if (e.getEntityType().name().equals("ENDERMAN")) {
|
||||
AreaConfig ac = getArea(e.getBlock().getWorld().getName(),
|
||||
e.getBlock().getX(), e.getBlock().getY(), e.getBlock().getZ());
|
||||
if (ac != null && ac.denyEnderTeleport) {
|
||||
e.setCancelled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== BlockIgniteEvent: 处理火把放置、打火石打地面等非玩家直接使用物品的场景 =====
|
||||
@@ -5456,24 +5475,14 @@ public class AreaProtection implements Listener {
|
||||
// ★ 公共设施连控:公共设施启用时,访客自动获得容器交互权限(跳过禁止检查)
|
||||
if (ac.isPublicBuilding) return;
|
||||
|
||||
// ★ 漏洞封堵:完全无该领地权限的外部玩家(getPermissionLevel 返回 null),
|
||||
// 无论容器管理(denyContainer)开关如何,一律禁止访问领地内容器。
|
||||
// 否则 denyContainer=false 时所有玩家(含未授权)开箱无任何限制。
|
||||
if (getPermissionLevel(p, ac) == null) {
|
||||
e.setCancelled(true);
|
||||
p.sendMessage("§c§l[区域防护] §f你没有该领地的访问权限,无法使用容器");
|
||||
return;
|
||||
}
|
||||
|
||||
// ★ 容器管理权限:denyContainer=true时非领主/管理员不能访问容器
|
||||
// ★ 容器管理权限:denyContainer控制访客能否访问容器
|
||||
// getEffectiveDeny 已内置 ADMIN/OWNER/VISITOR 豁免,
|
||||
// 对无权限玩家则走领地默认 denyContainer 配置
|
||||
if (getEffectiveDeny(p, ac, "denyContainer")) {
|
||||
e.setCancelled(true);
|
||||
p.sendMessage("§c§l[区域防护] §f此领地禁止访问容器");
|
||||
return;
|
||||
}
|
||||
|
||||
// ★ 容器交互已启用时,无权限级别的玩家也允许访问(访客权限)
|
||||
// 只有denyContainer=true时才检查权限级别
|
||||
}
|
||||
|
||||
public void loadWhitelists() {
|
||||
@@ -5691,6 +5700,76 @@ public class AreaProtection implements Listener {
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 耕地破坏(玩家在耕地上跳跃/掉落时触发)
|
||||
@EventHandler
|
||||
public void onFarmlandTrample(PlayerInteractEvent e) {
|
||||
if (e.getAction() != Action.PHYSICAL) return;
|
||||
Block block = e.getClickedBlock();
|
||||
if (block == null || block.getType() != Material.FARMLAND) return;
|
||||
Player p = e.getPlayer();
|
||||
AreaConfig ac = getArea(
|
||||
p.getWorld().getName(),
|
||||
block.getX(), block.getY(), block.getZ());
|
||||
if (ac != null && ac.denyFarmlandTrample) {
|
||||
e.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 末影人行为控制(禁止末影人传送进入领地、在领地内传送、自然进入领地)
|
||||
@EventHandler
|
||||
public void onEnderTeleport(EntityTeleportEvent e) {
|
||||
if (!e.getEntityType().name().equals("ENDERMAN")) return;
|
||||
Location from = e.getFrom();
|
||||
Location to = e.getTo();
|
||||
// 检查起点是否在禁止末影人传送的领地内
|
||||
if (from != null) {
|
||||
AreaConfig fromAc = getArea(from.getWorld().getName(),
|
||||
from.getBlockX(), from.getBlockY(), from.getBlockZ());
|
||||
if (fromAc != null && fromAc.denyEnderTeleport) {
|
||||
e.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 检查终点是否在禁止末影人传送的领地内
|
||||
if (to != null) {
|
||||
AreaConfig toAc = getArea(to.getWorld().getName(),
|
||||
to.getBlockX(), to.getBlockY(), to.getBlockZ());
|
||||
if (toAc != null && toAc.denyEnderTeleport) {
|
||||
e.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 如果起点和终点都不在禁止传送的领地内,不干预(让MC自身逻辑处理)
|
||||
// 注意:如果起点在允许传送的领地内,终点在禁止传送的领地内,已在上面处理
|
||||
// 如果起点在禁止传送的领地内,终点在允许传送的领地内,已在上面处理
|
||||
}
|
||||
|
||||
// ★ 末影人行为控制(禁止末影人AI寻路进入领地)
|
||||
@EventHandler
|
||||
public void onEndermanPathfind(EntityPathfindEvent e) {
|
||||
if (!e.getEntity().getType().name().equals("ENDERMAN")) return;
|
||||
Location loc = e.getLoc();
|
||||
if (loc == null) return;
|
||||
AreaConfig ac = getArea(loc.getWorld().getName(),
|
||||
loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
|
||||
if (ac != null && ac.denyEnderTeleport) {
|
||||
e.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 末影人生成控制(自然生成时禁止进入领地)
|
||||
@EventHandler
|
||||
public void onEndermanSpawn(CreatureSpawnEvent e) {
|
||||
if (!e.getEntityType().name().equals("ENDERMAN")) return;
|
||||
if (e.getSpawnReason() != CreatureSpawnEvent.SpawnReason.NATURAL) return;
|
||||
Location loc = e.getLocation();
|
||||
AreaConfig ac = getArea(loc.getWorld().getName(),
|
||||
loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
|
||||
if (ac != null && ac.denyEnderTeleport) {
|
||||
e.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 判断方块是否为农作物
|
||||
private boolean isCropBlock(Material mat) {
|
||||
String name = mat.name();
|
||||
@@ -7496,13 +7575,6 @@ public class AreaProtection implements Listener {
|
||||
if (online != null && online.isOnline()) {
|
||||
online.sendMessage("§a§l[防护] §f领地 §e" + finalAreaName + " §f等待期结束,已自动删除");
|
||||
}
|
||||
// 通知所有在线管理员
|
||||
for (Player admin : Bukkit.getOnlinePlayers()) {
|
||||
if (admin.getUniqueId().equals(uid)) continue;
|
||||
if (isAreaAdmin(admin)) {
|
||||
admin.sendMessage("§7[防护] 领地 §e" + finalAreaName + " §7等待期结束,已自动删除");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.runTaskLater(plugin, 1200L); // 60秒 = 1200 tick
|
||||
@@ -7624,7 +7696,7 @@ public class AreaProtection implements Listener {
|
||||
+ " 已在全局白名单中");
|
||||
return true;
|
||||
}
|
||||
globalPlayerWhitelist.add(name);
|
||||
globalPlayerWhitelist.add(name.toLowerCase());
|
||||
saveWhitelists();
|
||||
sender.sendMessage("§a已全局加白: " + name);
|
||||
return true;
|
||||
@@ -7639,7 +7711,7 @@ public class AreaProtection implements Listener {
|
||||
String areaName = parsed[0];
|
||||
String playerName = parsed[1];
|
||||
if (areaName.equalsIgnoreCase("global")) {
|
||||
globalPlayerWhitelist.add(playerName);
|
||||
globalPlayerWhitelist.add(playerName.toLowerCase());
|
||||
saveWhitelists();
|
||||
sender.sendMessage("§a已全局加白: "
|
||||
+ playerName);
|
||||
@@ -7648,7 +7720,7 @@ public class AreaProtection implements Listener {
|
||||
.computeIfAbsent(areaName,
|
||||
k -> ConcurrentHashMap
|
||||
.newKeySet());
|
||||
wl.add(playerName);
|
||||
wl.add(playerName.toLowerCase());
|
||||
saveWhitelists();
|
||||
sender.sendMessage("§a已加白: "
|
||||
+ playerName + " → " + areaName);
|
||||
@@ -8577,6 +8649,8 @@ public class AreaProtection implements Listener {
|
||||
int cx = (Math.min(ac.x1, ac.x2) + Math.max(ac.x1, ac.x2)) / 2;
|
||||
int cz = (Math.min(ac.z1, ac.z2) + Math.max(ac.z1, ac.z2)) / 2;
|
||||
World w = Bukkit.getWorld(ac.world);
|
||||
plugin.getLogger().info("[防护-TP] ★★★ 无传送点,传送到领地中心: 区域=" + ac.name
|
||||
+ " 世界=" + ac.world + " 中心=(" + cx + "," + cz + ") worldObj=" + (w != null ? "OK" : "NULL"));
|
||||
if (w == null) {
|
||||
sender.sendMessage("§c世界不存在: " + ac.world);
|
||||
return true;
|
||||
@@ -8601,8 +8675,35 @@ public class AreaProtection implements Listener {
|
||||
}
|
||||
} else {
|
||||
World w = Bukkit.getWorld(ac.warpWorld);
|
||||
plugin.getLogger().info("[防护-TP] ★★★ 有传送点: 区域=" + ac.name
|
||||
+ " warpWorld=" + ac.warpWorld + " warpPos=(" + ac.warpX + "," + ac.warpY + "," + ac.warpZ + ")"
|
||||
+ " worldObj=" + (w != null ? "OK" : "NULL"));
|
||||
if (w == null) {
|
||||
sender.sendMessage("§c传送点所在世界不存在");
|
||||
// 传送点世界不存在,回退到领地中心传送
|
||||
plugin.getLogger().warning("[防护-TP] 传送点世界不存在,回退到领地中心: " + ac.warpWorld);
|
||||
int cx = (Math.min(ac.x1, ac.x2) + Math.max(ac.x1, ac.x2)) / 2;
|
||||
int cz = (Math.min(ac.z1, ac.z2) + Math.max(ac.z1, ac.z2)) / 2;
|
||||
World fallback = Bukkit.getWorld(ac.world);
|
||||
if (fallback == null) {
|
||||
sender.sendMessage("§c世界不存在: " + ac.world);
|
||||
return true;
|
||||
}
|
||||
// 找到安全位置
|
||||
Location safeLoc = null;
|
||||
for (int y = fallback.getMaxHeight(); y >= fallback.getMinHeight(); y--) {
|
||||
Location check = new Location(fallback, cx, y, cz);
|
||||
if (!check.getBlock().getType().isSolid()) continue;
|
||||
if (check.clone().add(0, 1, 0).getBlock().getType().isSolid()) continue;
|
||||
if (check.clone().add(0, 2, 0).getBlock().getType().isSolid()) continue;
|
||||
safeLoc = check.clone().add(0, 1, 0);
|
||||
break;
|
||||
}
|
||||
if (safeLoc != null) {
|
||||
p.teleport(safeLoc);
|
||||
sender.sendMessage("§a§l[防护] §f已传送到 §e" + ac.name + " §f安全位置(传送点世界不存在)");
|
||||
} else {
|
||||
sender.sendMessage("§c§l[防护] §f无法找到安全传送位置,请为该领地设置传送点 (/protect settp)");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Location warp = new Location(w, ac.warpX, ac.warpY, ac.warpZ, ac.warpYaw, ac.warpPitch);
|
||||
@@ -8737,8 +8838,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) "
|
||||
+ "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) "
|
||||
+ "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, "
|
||||
@@ -8751,7 +8852,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");
|
||||
+ "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");
|
||||
|
||||
stmt.setString(1, ac.name);
|
||||
stmt.setString(2, ac.owner != null ? ac.owner : "");
|
||||
@@ -8820,6 +8921,8 @@ public class AreaProtection implements Listener {
|
||||
stmt.setInt(65, ac.denySignEdit ? 1 : 0);
|
||||
stmt.setInt(66, ac.isPublicBuilding ? 1 : 0);
|
||||
stmt.setInt(67, ac.allowVisitorTeleport ? 1 : 0);
|
||||
stmt.setInt(68, ac.denyFarmlandTrample ? 1 : 0);
|
||||
stmt.setInt(69, ac.denyEnderTeleport ? 1 : 0);
|
||||
stmt.executeUpdate();
|
||||
stmt.close();
|
||||
// ★ 领地设置变更:立即触发PHP同步(防抖10秒)
|
||||
@@ -10425,7 +10528,7 @@ public class AreaProtection implements Listener {
|
||||
|
||||
public boolean addPlayerToGlobalWhitelist(
|
||||
String playerName) {
|
||||
boolean ok = globalPlayerWhitelist.add(playerName);
|
||||
boolean ok = globalPlayerWhitelist.add(playerName.toLowerCase());
|
||||
if (ok) saveWhitelists();
|
||||
return ok;
|
||||
}
|
||||
@@ -10433,6 +10536,7 @@ public class AreaProtection implements Listener {
|
||||
public boolean removePlayerFromGlobalWhitelist(
|
||||
String playerName) {
|
||||
boolean ok = globalPlayerWhitelist.remove(playerName);
|
||||
if (!ok) ok = globalPlayerWhitelist.remove(playerName.toLowerCase());
|
||||
if (ok) saveWhitelists();
|
||||
return ok;
|
||||
}
|
||||
@@ -10441,7 +10545,7 @@ public class AreaProtection implements Listener {
|
||||
String areaName, String playerName) {
|
||||
Set<String> wl =
|
||||
areaPlayerWhitelist.get(areaName);
|
||||
return wl != null && wl.contains(playerName);
|
||||
return wl != null && wl.contains(playerName.toLowerCase());
|
||||
}
|
||||
|
||||
public boolean addPlayerToAreaWhitelist(
|
||||
@@ -10449,7 +10553,7 @@ public class AreaProtection implements Listener {
|
||||
Set<String> set = areaPlayerWhitelist
|
||||
.computeIfAbsent(areaName,
|
||||
k -> ConcurrentHashMap.newKeySet());
|
||||
boolean ok = set.add(playerName);
|
||||
boolean ok = set.add(playerName.toLowerCase());
|
||||
if (ok) saveWhitelists();
|
||||
return ok;
|
||||
}
|
||||
@@ -11419,6 +11523,10 @@ public class AreaProtection implements Listener {
|
||||
public boolean denyLead = false;
|
||||
// ★ 农作物状态检测(播种&收获)
|
||||
public boolean denyCropHarvest = false;
|
||||
// ★ 耕地破坏(玩家跳跃踩踏耕地 → 变为泥土)
|
||||
public boolean denyFarmlandTrample = false;
|
||||
// ★ 末影人传送控制
|
||||
public boolean denyEnderTeleport = false;
|
||||
// ★ 采集羊毛和乐魂挽具
|
||||
public boolean denyWoolShear = false;
|
||||
// ★ 投喂动物
|
||||
|
||||
@@ -13,7 +13,7 @@ public class BondManager {
|
||||
|
||||
// ===== 交易监听器 =====
|
||||
public interface TransactionListener {
|
||||
void onTransaction(String playerName, String type, int amount);
|
||||
void onTransaction(String playerName, String type, int amount, String targetPlayer);
|
||||
}
|
||||
private final List<TransactionListener> transactionListeners = new ArrayList<>();
|
||||
|
||||
@@ -21,10 +21,10 @@ public class BondManager {
|
||||
transactionListeners.add(listener);
|
||||
}
|
||||
|
||||
private void fireTransactionEvent(String playerName, String type, int amount) {
|
||||
private void fireTransactionEvent(String playerName, String type, int amount, String targetPlayer) {
|
||||
for (TransactionListener listener : transactionListeners) {
|
||||
try {
|
||||
listener.onTransaction(playerName, type, amount);
|
||||
listener.onTransaction(playerName, type, amount, targetPlayer);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
@@ -382,7 +382,7 @@ public class BondManager {
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
// 触发交易监听器
|
||||
fireTransactionEvent(player, type, amount);
|
||||
fireTransactionEvent(player, type, amount, targetPlayer);
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -49,6 +49,10 @@ public class ConfigManager {
|
||||
public double providerEconomyPerScore = 50.0;
|
||||
public int requesterPointsReward = 10;
|
||||
|
||||
// ===== 密码错误封禁配置 =====
|
||||
public int passwordFailThreshold = 5; // 密码错误阈值(达到后封禁)
|
||||
public int passwordBanDuration = 300; // 封禁时长(秒),默认5分钟
|
||||
|
||||
public boolean garbageEnabled = true;
|
||||
public int garbageInterval = 300;
|
||||
public int garbageMaxRounds = 1;
|
||||
@@ -395,6 +399,8 @@ public class ConfigManager {
|
||||
{"区域防护_最大领地数", String.valueOf(areaProtectMaxLands)},
|
||||
{"传送_请求有效秒", String.valueOf(tpRequestValidSeconds)},
|
||||
{"传送_发送间隔秒", String.valueOf(tpSendIntervalSeconds)},
|
||||
{"密码错误封禁阈值", String.valueOf(passwordFailThreshold)},
|
||||
{"密码错误封禁时长秒", String.valueOf(passwordBanDuration)},
|
||||
};
|
||||
for (String[] nk : newKeys) {
|
||||
if (!m.containsKey(nk[0])) { hasNewKey = true; }
|
||||
@@ -435,6 +441,10 @@ public class ConfigManager {
|
||||
providerEconomyPerScore = parseDouble(m.getOrDefault("经济奖励每评分", "50.0"), 50.0);
|
||||
requesterPointsReward = parseInt(m.getOrDefault("报单人积分奖励", "10"), 10);
|
||||
|
||||
// 密码错误封禁配置
|
||||
passwordFailThreshold = parseInt(m.getOrDefault("密码错误封禁阈值", "5"), 5);
|
||||
passwordBanDuration = parseInt(m.getOrDefault("密码错误封禁时长秒", "300"), 300);
|
||||
|
||||
garbageEnabled = Boolean.parseBoolean(m.getOrDefault("垃圾站_启用", "true"));
|
||||
garbageInterval = parseInt(m.getOrDefault("垃圾站_清理间隔秒", "300"), 300);
|
||||
garbageMaxRounds = parseInt(m.getOrDefault("垃圾站_保留轮数", "1"), 1);
|
||||
@@ -542,6 +552,8 @@ public class ConfigManager {
|
||||
managed.put("奖励发放方式", "债券");
|
||||
managed.put("传送_请求有效秒", String.valueOf(tpRequestValidSeconds));
|
||||
managed.put("传送_发送间隔秒", String.valueOf(tpSendIntervalSeconds));
|
||||
managed.put("密码错误封禁阈值", String.valueOf(passwordFailThreshold));
|
||||
managed.put("密码错误封禁时长秒", String.valueOf(passwordBanDuration));
|
||||
|
||||
// ★ 保留非ConfigManager管理的配置行(如web通信-*等)
|
||||
// ★ 同时保留连续的注释块(如 "# ===== Web通信配置 =====")
|
||||
@@ -632,6 +644,9 @@ public class ConfigManager {
|
||||
L.add("区域防护_最大领地数=3");
|
||||
L.add("传送_请求有效秒=90");
|
||||
L.add("传送_发送间隔秒=10");
|
||||
// 密码错误封禁
|
||||
L.add("密码错误封禁阈值=5");
|
||||
L.add("密码错误封禁时长秒=300");
|
||||
// Web通信配置(默认禁用,需手动开启)
|
||||
L.add("");
|
||||
L.add("# ===== Web通信配置 =====");
|
||||
|
||||
@@ -107,6 +107,9 @@ public class DatabaseManager {
|
||||
// 禁言理由
|
||||
safeAdd(st, "mute_reason",
|
||||
"TEXT DEFAULT ''");
|
||||
// ★ 正版OAuth登录IP(用于异地登录风控)
|
||||
safeAdd(st, "last_oauth_ip",
|
||||
"TEXT DEFAULT ''");
|
||||
|
||||
// ===== 背包备份表:安全迁移 =====
|
||||
// 检查旧表是否存在(用 inventory_data 列判断)
|
||||
@@ -225,6 +228,25 @@ public class DatabaseManager {
|
||||
+ "PRIMARY KEY(player_name, region_name)"
|
||||
+ ")");
|
||||
|
||||
// PVP竞技场会话表(每局PVP世界)
|
||||
st.execute("CREATE TABLE IF NOT EXISTS "
|
||||
+ "pvp_arena_sessions ("
|
||||
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
+ "session_number INTEGER NOT NULL UNIQUE,"
|
||||
+ "world_name TEXT NOT NULL,"
|
||||
+ "created_at INTEGER NOT NULL"
|
||||
+ ")");
|
||||
// PVP竞技场战绩表(按会话+玩家)
|
||||
st.execute("CREATE TABLE IF NOT EXISTS "
|
||||
+ "pvp_arena_stats ("
|
||||
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
+ "session_id INTEGER NOT NULL,"
|
||||
+ "player_uuid TEXT NOT NULL,"
|
||||
+ "player_name TEXT NOT NULL,"
|
||||
+ "kills INTEGER DEFAULT 0,"
|
||||
+ "deaths INTEGER DEFAULT 0,"
|
||||
+ "UNIQUE(session_id, player_uuid)"
|
||||
+ ")");
|
||||
|
||||
// 菜单图标表
|
||||
st.execute("CREATE TABLE IF NOT EXISTS "
|
||||
@@ -1477,6 +1499,177 @@ public class DatabaseManager {
|
||||
return list;
|
||||
}
|
||||
|
||||
// ==================== PVP竞技场会话 & 战绩持久化 ====================
|
||||
|
||||
/** 创建新PVP竞技场会话,返回生成的id */
|
||||
public int createArenaSession(String worldName) {
|
||||
try {
|
||||
// 先查最大session_number
|
||||
int maxNum = 0;
|
||||
Statement st = db.createStatement();
|
||||
ResultSet rs = st.executeQuery("SELECT COALESCE(MAX(session_number),0) FROM pvp_arena_sessions");
|
||||
if (rs.next()) maxNum = rs.getInt(1);
|
||||
rs.close();
|
||||
st.close();
|
||||
int newNum = maxNum + 1;
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"INSERT INTO pvp_arena_sessions (session_number, world_name, created_at) VALUES (?,?,?)",
|
||||
PreparedStatement.RETURN_GENERATED_KEYS);
|
||||
ps.setInt(1, newNum);
|
||||
ps.setString(2, worldName);
|
||||
ps.setLong(3, System.currentTimeMillis());
|
||||
ps.executeUpdate();
|
||||
ResultSet keys = ps.getGeneratedKeys();
|
||||
int id = keys.next() ? keys.getInt(1) : 1;
|
||||
keys.close();
|
||||
ps.close();
|
||||
return id;
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取指定会话编号的信息(超过3天返回null) */
|
||||
public Map<String, Object> getArenaSessionByNumber(int number) {
|
||||
long threeDaysMs = 3L * 24 * 60 * 60 * 1000;
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT id, session_number, world_name, created_at FROM pvp_arena_sessions WHERE session_number=? AND ? - created_at <= ?");
|
||||
long now = System.currentTimeMillis();
|
||||
ps.setInt(1, number);
|
||||
ps.setLong(2, now);
|
||||
ps.setLong(3, threeDaysMs);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
if (rs.next()) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("id", rs.getInt("id"));
|
||||
row.put("session_number", rs.getInt("session_number"));
|
||||
row.put("world_name", rs.getString("world_name"));
|
||||
row.put("created_at", rs.getLong("created_at"));
|
||||
rs.close();
|
||||
ps.close();
|
||||
return row;
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 获取所有竞技场会话列表(按编号降序) */
|
||||
public List<Map<String, Object>> getAllArenaSessions() {
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
try {
|
||||
Statement st = db.createStatement();
|
||||
ResultSet rs = st.executeQuery(
|
||||
"SELECT id, session_number, world_name, created_at FROM pvp_arena_sessions ORDER BY session_number DESC");
|
||||
while (rs.next()) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("id", rs.getInt("id"));
|
||||
row.put("session_number", rs.getInt("session_number"));
|
||||
row.put("world_name", rs.getString("world_name"));
|
||||
row.put("created_at", rs.getLong("created_at"));
|
||||
list.add(row);
|
||||
}
|
||||
rs.close();
|
||||
st.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 记录竞技场击杀(INSERT OR IGNORE + UPDATE) */
|
||||
public void recordArenaKill(int sessionId, String uuid, String name) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"INSERT OR IGNORE INTO pvp_arena_stats (session_id, player_uuid, player_name, kills, deaths) VALUES (?,?,?,0,0)");
|
||||
ps.setInt(1, sessionId);
|
||||
ps.setString(2, uuid);
|
||||
ps.setString(3, name);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
ps = db.prepareStatement(
|
||||
"UPDATE pvp_arena_stats SET kills = kills + 1, player_name=? WHERE session_id=? AND player_uuid=?");
|
||||
ps.setString(1, name);
|
||||
ps.setInt(2, sessionId);
|
||||
ps.setString(3, uuid);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/** 记录竞技场死亡 */
|
||||
public void recordArenaDeath(int sessionId, String uuid, String name) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"INSERT OR IGNORE INTO pvp_arena_stats (session_id, player_uuid, player_name, kills, deaths) VALUES (?,?,?,0,0)");
|
||||
ps.setInt(1, sessionId);
|
||||
ps.setString(2, uuid);
|
||||
ps.setString(3, name);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
ps = db.prepareStatement(
|
||||
"UPDATE pvp_arena_stats SET deaths = deaths + 1, player_name=? WHERE session_id=? AND player_uuid=?");
|
||||
ps.setString(1, name);
|
||||
ps.setInt(2, sessionId);
|
||||
ps.setString(3, uuid);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取某玩家所有竞技场会话的总战绩 */
|
||||
public Map<String, Object> getArenaTotalStats(String uuid) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("kills", 0);
|
||||
result.put("deaths", 0);
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT COALESCE(SUM(kills),0) AS total_kills, COALESCE(SUM(deaths),0) AS total_deaths FROM pvp_arena_stats WHERE player_uuid=?");
|
||||
ps.setString(1, uuid);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
if (rs.next()) {
|
||||
result.put("kills", rs.getInt("total_kills"));
|
||||
result.put("deaths", rs.getInt("total_deaths"));
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 获取某玩家在指定会话编号中的战绩 */
|
||||
public Map<String, Object> getArenaSessionStats(String uuid, int sessionId) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("kills", 0);
|
||||
result.put("deaths", 0);
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT kills, deaths FROM pvp_arena_stats WHERE session_id=? AND player_uuid=?");
|
||||
ps.setInt(1, sessionId);
|
||||
ps.setString(2, uuid);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
if (rs.next()) {
|
||||
result.put("kills", rs.getInt("kills"));
|
||||
result.put("deaths", rs.getInt("deaths"));
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ==================== 安全报警 ====================
|
||||
|
||||
@@ -2349,22 +2542,116 @@ public class DatabaseManager {
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 查询某坐标最近的容器记录(按时间倒序) */
|
||||
public List<Map<String, Object>> getLandContainerLogAt(
|
||||
String world, int x, int y, int z, int limit) {
|
||||
/** 查询某坐标指定天数前的方块记录(daysAgo=0今天,1昨天,-1所有) */
|
||||
public List<Map<String, Object>> getLandBlockLogAt(
|
||||
String world, int x, int y, int z, int limit, int daysAgo) {
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT time, player_name, action, container_type,"
|
||||
+ " detail, amount"
|
||||
+ " FROM land_container_log"
|
||||
+ " WHERE world=? AND x=? AND y=? AND z=?"
|
||||
+ " ORDER BY time DESC LIMIT ?");
|
||||
StringBuilder sql = new StringBuilder(
|
||||
"SELECT time, player_name, action, material, detail"
|
||||
+ " FROM land_block_log"
|
||||
+ " WHERE world=? AND x=? AND y=? AND z=?");
|
||||
if (daysAgo >= 0) {
|
||||
java.util.Calendar cal = java.util.Calendar.getInstance();
|
||||
cal.set(java.util.Calendar.HOUR_OF_DAY, 0);
|
||||
cal.set(java.util.Calendar.MINUTE, 0);
|
||||
cal.set(java.util.Calendar.SECOND, 0);
|
||||
cal.set(java.util.Calendar.MILLISECOND, 0);
|
||||
cal.add(java.util.Calendar.DAY_OF_YEAR, -daysAgo);
|
||||
long dayStart = cal.getTimeInMillis();
|
||||
cal.add(java.util.Calendar.DAY_OF_YEAR, 1);
|
||||
long dayEnd = cal.getTimeInMillis();
|
||||
sql.append(" AND time >= ? AND time < ?");
|
||||
}
|
||||
sql.append(" ORDER BY time DESC LIMIT ?");
|
||||
PreparedStatement ps = db.prepareStatement(sql.toString());
|
||||
ps.setString(1, world);
|
||||
ps.setInt(2, x);
|
||||
ps.setInt(3, y);
|
||||
ps.setInt(4, z);
|
||||
ps.setInt(5, limit > 0 ? limit : 20);
|
||||
int idx = 5;
|
||||
if (daysAgo >= 0) {
|
||||
java.util.Calendar cal = java.util.Calendar.getInstance();
|
||||
cal.set(java.util.Calendar.HOUR_OF_DAY, 0);
|
||||
cal.set(java.util.Calendar.MINUTE, 0);
|
||||
cal.set(java.util.Calendar.SECOND, 0);
|
||||
cal.set(java.util.Calendar.MILLISECOND, 0);
|
||||
cal.add(java.util.Calendar.DAY_OF_YEAR, -daysAgo);
|
||||
long dayStart = cal.getTimeInMillis();
|
||||
cal.add(java.util.Calendar.DAY_OF_YEAR, 1);
|
||||
long dayEnd = cal.getTimeInMillis();
|
||||
ps.setLong(idx++, dayStart);
|
||||
ps.setLong(idx++, dayEnd);
|
||||
}
|
||||
ps.setInt(idx, limit > 0 ? limit : 20);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
while (rs.next()) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("time", rs.getLong("time"));
|
||||
row.put("player_name", rs.getString("player_name"));
|
||||
row.put("action", rs.getString("action"));
|
||||
row.put("material", rs.getString("material"));
|
||||
row.put("detail", rs.getString("detail"));
|
||||
list.add(row);
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 查询某坐标最近的容器记录(按时间倒序) */
|
||||
public List<Map<String, Object>> getLandContainerLogAt(
|
||||
String world, int x, int y, int z, int limit) {
|
||||
return getLandContainerLogAt(world, x, y, z, limit, -1);
|
||||
}
|
||||
|
||||
/** 查询某坐标指定天数前的容器记录(daysAgo=0今天,1昨天,-1所有) */
|
||||
public List<Map<String, Object>> getLandContainerLogAt(
|
||||
String world, int x, int y, int z, int limit, int daysAgo) {
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
try {
|
||||
StringBuilder sql = new StringBuilder(
|
||||
"SELECT time, player_name, action, container_type,"
|
||||
+ " detail, amount"
|
||||
+ " FROM land_container_log"
|
||||
+ " WHERE world=? AND x=? AND y=? AND z=?");
|
||||
if (daysAgo >= 0) {
|
||||
// 计算当天起始和结束毫秒时间戳
|
||||
java.util.Calendar cal = java.util.Calendar.getInstance();
|
||||
cal.set(java.util.Calendar.HOUR_OF_DAY, 0);
|
||||
cal.set(java.util.Calendar.MINUTE, 0);
|
||||
cal.set(java.util.Calendar.SECOND, 0);
|
||||
cal.set(java.util.Calendar.MILLISECOND, 0);
|
||||
cal.add(java.util.Calendar.DAY_OF_YEAR, -daysAgo);
|
||||
long dayStart = cal.getTimeInMillis();
|
||||
cal.add(java.util.Calendar.DAY_OF_YEAR, 1);
|
||||
long dayEnd = cal.getTimeInMillis();
|
||||
sql.append(" AND time >= ? AND time < ?");
|
||||
}
|
||||
sql.append(" ORDER BY time DESC LIMIT ?");
|
||||
PreparedStatement ps = db.prepareStatement(sql.toString());
|
||||
ps.setString(1, world);
|
||||
ps.setInt(2, x);
|
||||
ps.setInt(3, y);
|
||||
ps.setInt(4, z);
|
||||
int idx = 5;
|
||||
if (daysAgo >= 0) {
|
||||
java.util.Calendar cal = java.util.Calendar.getInstance();
|
||||
cal.set(java.util.Calendar.HOUR_OF_DAY, 0);
|
||||
cal.set(java.util.Calendar.MINUTE, 0);
|
||||
cal.set(java.util.Calendar.SECOND, 0);
|
||||
cal.set(java.util.Calendar.MILLISECOND, 0);
|
||||
cal.add(java.util.Calendar.DAY_OF_YEAR, -daysAgo);
|
||||
long dayStart = cal.getTimeInMillis();
|
||||
cal.add(java.util.Calendar.DAY_OF_YEAR, 1);
|
||||
long dayEnd = cal.getTimeInMillis();
|
||||
ps.setLong(idx++, dayStart);
|
||||
ps.setLong(idx++, dayEnd);
|
||||
}
|
||||
ps.setInt(idx, limit > 0 ? limit : 20);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
while (rs.next()) {
|
||||
Map<String, Object> row =
|
||||
|
||||
@@ -29,6 +29,10 @@ public class IPGroupManager {
|
||||
return can;
|
||||
}
|
||||
|
||||
public int getMaxAccounts() {
|
||||
return maxAccounts;
|
||||
}
|
||||
|
||||
public int getAccountCount(String ip) {
|
||||
if (ip == null || ip.isEmpty()) return 0;
|
||||
return queryAccountsByIP(ip).size();
|
||||
|
||||
@@ -16,6 +16,10 @@ import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.event.HoverEvent;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -40,6 +44,9 @@ public class LandRecordManager implements Listener {
|
||||
/** 回声碎片物品材质(1.19+ 存在) */
|
||||
public static final Material ECHO_SHARD_MATERIAL = Material.ECHO_SHARD;
|
||||
|
||||
/** 玩家最后一次查询的坐标缓存,翻页导航时复用,避免 getTargetBlockExact 不准 */
|
||||
private final Map<UUID, Location> lastQueryLocations = new ConcurrentHashMap<>();
|
||||
|
||||
/** 记录查询默认条数 */
|
||||
private static final int QUERY_LIMIT = 20;
|
||||
/** 记录保留天数 */
|
||||
@@ -289,24 +296,37 @@ public class LandRecordManager implements Listener {
|
||||
p.sendMessage("§c§l[回声碎片] §f你面前没有可查询的方块或容器");
|
||||
return;
|
||||
}
|
||||
queryAndPrint(p, target.getLocation());
|
||||
Location loc = target.getLocation();
|
||||
lastQueryLocations.put(p.getUniqueId(), loc);
|
||||
queryAndPrint(p, loc);
|
||||
}
|
||||
|
||||
private void queryAndPrint(Player p, Location loc) {
|
||||
queryAndPrint(p, loc, 0);
|
||||
}
|
||||
|
||||
private void queryAndPrint(Player p, Location loc, int page) {
|
||||
String world = loc.getWorld().getName();
|
||||
int x = loc.getBlockX(), y = loc.getBlockY(),
|
||||
z = loc.getBlockZ();
|
||||
|
||||
// 按天分页:page=0今日,1昨日,2前日...
|
||||
List<Map<String, Object>> blocks =
|
||||
plugin.getDb().getLandBlockLogAt(
|
||||
world, x, y, z, QUERY_LIMIT);
|
||||
world, x, y, z, QUERY_LIMIT, page);
|
||||
// 按天分页:page=0今日,1昨日,2前日...
|
||||
List<Map<String, Object>> containers =
|
||||
plugin.getDb().getLandContainerLogAt(
|
||||
world, x, y, z, QUERY_LIMIT);
|
||||
world, x, y, z, QUERY_LIMIT, page);
|
||||
|
||||
if (blocks.isEmpty() && containers.isEmpty()) {
|
||||
p.sendMessage("§6§l[回声碎片] §f坐标 ("
|
||||
+ x + "," + y + "," + z + ") 暂无领地记录");
|
||||
if (page > 0) {
|
||||
p.sendMessage("§6§l[回声碎片] §e坐标 ("
|
||||
+ x + "," + y + "," + z + ") 第" + (page + 1) + "页无记录");
|
||||
} else {
|
||||
p.sendMessage("§6§l[回声碎片] §f坐标 ("
|
||||
+ x + "," + y + "," + z + ") 暂无领地记录");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -319,19 +339,24 @@ public class LandRecordManager implements Listener {
|
||||
if ("take".equals(r.get("action")) || "put".equals(r.get("action"))) {
|
||||
materials.add((String) r.get("detail"));
|
||||
}
|
||||
// 容器类型(如 CHEST)也要翻译
|
||||
String ct = (String) r.get("container_type");
|
||||
if (ct != null && !ct.isEmpty()) {
|
||||
materials.add(ct);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查下一页是否有数据(决定是否显示"下一页"链接)
|
||||
boolean hasNext = false;
|
||||
if (!plugin.getDb().getLandBlockLogAt(world, x, y, z, 1, page + 1).isEmpty()) hasNext = true;
|
||||
if (!plugin.getDb().getLandContainerLogAt(world, x, y, z, 1, page + 1).isEmpty()) hasNext = true;
|
||||
|
||||
final int currentPage = page;
|
||||
final boolean hasNextPage = hasNext;
|
||||
// 异步批量翻译
|
||||
MaterialTranslator.translateBatch(materials)
|
||||
.thenAccept(translations -> {
|
||||
// 回到主线程发送消息
|
||||
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||
printResults(p, x, y, z, blocks, containers, translations);
|
||||
printResults(p, x, y, z, blocks, containers, translations, currentPage, hasNextPage);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -340,8 +365,17 @@ public class LandRecordManager implements Listener {
|
||||
List<Map<String, Object>> blocks,
|
||||
List<Map<String, Object>> containers,
|
||||
Map<String, String> translations) {
|
||||
printResults(p, x, y, z, blocks, containers, translations, 0, false);
|
||||
}
|
||||
|
||||
private void printResults(Player p, int x, int y, int z,
|
||||
List<Map<String, Object>> blocks,
|
||||
List<Map<String, Object>> containers,
|
||||
Map<String, String> translations,
|
||||
int page, boolean hasNext) {
|
||||
String pageLabel = page == 0 ? "今日" : (page == 1 ? "昨日" : (page + 1) + "日前");
|
||||
p.sendMessage("§6§l[回声碎片] §e坐标 ("
|
||||
+ x + "," + y + "," + z + ") 的记录:");
|
||||
+ x + "," + y + "," + z + ") 的记录§7(" + pageLabel + "):");
|
||||
|
||||
if (!blocks.isEmpty()) {
|
||||
p.sendMessage("§b— 方块记录 —");
|
||||
@@ -368,7 +402,6 @@ public class LandRecordManager implements Listener {
|
||||
.append(" §f").append(r.get("player_name"))
|
||||
.append(" ").append(act)
|
||||
.append(" §e").append(mat).append(" §a").append(zh);
|
||||
// ★ 告示牌内容显示
|
||||
String detail = (String) r.get("detail");
|
||||
if (detail != null && !detail.isEmpty()
|
||||
&& ("sign_place".equals(action) || "sign_edit".equals(action))) {
|
||||
@@ -379,7 +412,7 @@ public class LandRecordManager implements Listener {
|
||||
}
|
||||
|
||||
if (!containers.isEmpty()) {
|
||||
p.sendMessage("§b— 容器记录 —");
|
||||
p.sendMessage("§b— 容器记录 §7(" + pageLabel + ")—");
|
||||
for (Map<String, Object> r : containers) {
|
||||
String act = (String) r.get("action");
|
||||
if ("open".equals(act)) {
|
||||
@@ -405,6 +438,24 @@ public class LandRecordManager implements Listener {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 可点击翻页导航
|
||||
Component nav = Component.text(" §7[ ");
|
||||
if (page > 0) {
|
||||
nav = nav.append(Component.text("§e← 上一页")
|
||||
.clickEvent(ClickEvent.runCommand("/landrec log " + page))
|
||||
.hoverEvent(HoverEvent.showText(Component.text("§7点击查看前一日记录"))));
|
||||
nav = nav.append(Component.text(" §7| "));
|
||||
}
|
||||
nav = nav.append(Component.text("§f第" + (page + 1) + "页"));
|
||||
if (hasNext) {
|
||||
nav = nav.append(Component.text(" §7| ")
|
||||
.append(Component.text("§e下一页 →")
|
||||
.clickEvent(ClickEvent.runCommand("/landrec log " + (page + 2)))
|
||||
.hoverEvent(HoverEvent.showText(Component.text("§7点击查看后一日记录")))));
|
||||
}
|
||||
nav = nav.append(Component.text(" §7]"));
|
||||
p.sendMessage(nav);
|
||||
}
|
||||
|
||||
private String fmt(long ms) {
|
||||
@@ -421,7 +472,7 @@ public class LandRecordManager implements Listener {
|
||||
}
|
||||
String sub = args[0];
|
||||
if (sub.equalsIgnoreCase("give")) {
|
||||
// 发放权限:给自己(self)任意玩家可发;发给他人需 OP 或领地管理员
|
||||
// 发放权限...
|
||||
boolean isOp = sender.isOp();
|
||||
boolean isAreaAdmin = (sender instanceof Player)
|
||||
&& plugin.areaProtection.isAreaAdmin((Player) sender);
|
||||
@@ -448,6 +499,42 @@ public class LandRecordManager implements Listener {
|
||||
sender.sendMessage("§a§l[回声碎片] §f已发放回声碎片给 " + target.getName());
|
||||
return true;
|
||||
}
|
||||
if (sub.equalsIgnoreCase("log")) {
|
||||
if (!(sender instanceof Player)) {
|
||||
sender.sendMessage("§c§l[回声碎片] §f此命令仅限玩家使用");
|
||||
return true;
|
||||
}
|
||||
Player p = (Player) sender;
|
||||
int page = 0; // 默认今日
|
||||
boolean isNav = false; // true=从翻页链接跳转,复用 lastQueryLocations
|
||||
if (args.length >= 2) {
|
||||
try {
|
||||
page = Math.max(0, Integer.parseInt(args[1]) - 1);
|
||||
isNav = true;
|
||||
} catch (NumberFormatException ignored) {
|
||||
sender.sendMessage("§c§l[回声碎片] §f页码格式错误");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 翻页时复用上次查询坐标,避免 getTargetBlockExact 玩家未对准
|
||||
Location navLoc = isNav ? lastQueryLocations.get(p.getUniqueId()) : null;
|
||||
if (navLoc != null) {
|
||||
queryAndPrint(p, navLoc, page);
|
||||
return true;
|
||||
}
|
||||
// 首次查询或缓存过期,取准心方块
|
||||
Block target = p.getTargetBlockExact(6);
|
||||
if (target == null || target.getType() == Material.AIR) {
|
||||
sender.sendMessage("§c§l[回声碎片] §f你面前没有可查询的方块或容器");
|
||||
return true;
|
||||
}
|
||||
Location loc = target.getLocation();
|
||||
if (!isNav) {
|
||||
lastQueryLocations.put(p.getUniqueId(), loc);
|
||||
}
|
||||
queryAndPrint(p, loc, page);
|
||||
return true;
|
||||
}
|
||||
sendHelp(sender);
|
||||
return true;
|
||||
}
|
||||
@@ -456,6 +543,8 @@ public class LandRecordManager implements Listener {
|
||||
sender.sendMessage("§6§l[回声碎片] §e指令:");
|
||||
sender.sendMessage(" §7/landrec give §f- 给自己发放一枚回声碎片");
|
||||
sender.sendMessage(" §7/landrec give <玩家> §f- 向他人发放(需 OP/领地管理员)");
|
||||
sender.sendMessage(" §7手持回声碎片右键/左键方块或容器 §f- 查看该处记录");
|
||||
sender.sendMessage(" §7/landrec log §f- 查看当前方块今日记录");
|
||||
sender.sendMessage(" §7/landrec log <页码> §f- 查看指定日期的记录(1=今日 2=昨日 ...)");
|
||||
sender.sendMessage(" §7手持回声碎片右键/左键方块或容器 §f- 查看该处今日记录");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +154,16 @@ public class LoginManager {
|
||||
.msg("not_registered"));
|
||||
return true;
|
||||
}
|
||||
|
||||
// ★ 风控检查:封禁期内拒绝登录
|
||||
if (plugin.getRiskControl().isBanned(name)) {
|
||||
int remaining = plugin.getRiskControl()
|
||||
.getBanRemainingSeconds(name);
|
||||
p.sendMessage("§c§l[安全风控] §f账号因密码错误过多已被封禁,"
|
||||
+ "剩余 §e" + remaining + " §f秒");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (args.length < 1) {
|
||||
p.sendMessage("§c用法: /login <密码>");
|
||||
return true;
|
||||
@@ -176,6 +186,8 @@ public class LoginManager {
|
||||
plugin.getDb().checkPassword(
|
||||
name, hash);
|
||||
if (matchMain) {
|
||||
// ★ 登录成功,重置风控
|
||||
plugin.getRiskControl().onLoginSuccess(name);
|
||||
plugin.getLoggedIn().add(name);
|
||||
// ★ 记录Java手动登录:5分钟内重连可直接放行(检查点1)
|
||||
if (plugin.webManager != null) {
|
||||
@@ -225,6 +237,8 @@ public class LoginManager {
|
||||
.checkPasswordWithFallback(
|
||||
name, hash);
|
||||
if (pwdResult != null) {
|
||||
// ★ 临时密码登录成功,也重置风控
|
||||
plugin.getRiskControl().onLoginSuccess(name);
|
||||
plugin.getLoggedIn().add(name);
|
||||
// ★ 记录Java手动登录:5分钟内重连可直接放行(检查点1)
|
||||
if (plugin.webManager != null) {
|
||||
@@ -256,8 +270,19 @@ public class LoginManager {
|
||||
}
|
||||
|
||||
|
||||
// ★ 密码错误,记录风控
|
||||
int remaining = plugin.getRiskControl()
|
||||
.recordFailAttempt(p);
|
||||
if (remaining < 0) {
|
||||
// 已被封禁(triggerBan内部已踢出)
|
||||
return true;
|
||||
}
|
||||
p.sendMessage(plugin.getConfig2()
|
||||
.msg("login_failed"));
|
||||
if (remaining > 0) {
|
||||
p.sendMessage("§c§l[风控] §f剩余 §e"
|
||||
+ remaining + " §f次尝试机会");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().severe(
|
||||
"[Sdf1_login] handleLogin异常: "
|
||||
|
||||
+7933
-7795
File diff suppressed because it is too large
Load Diff
@@ -18,9 +18,17 @@ import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.inventory.meta.PotionMeta;
|
||||
import org.bukkit.scoreboard.*;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.bukkit.util.io.BukkitObjectOutputStream;
|
||||
|
||||
import org.bukkit.entity.Egg;
|
||||
import org.bukkit.entity.SmallFireball;
|
||||
import org.bukkit.event.block.Action;
|
||||
import org.bukkit.event.entity.ProjectileHitEvent;
|
||||
import org.bukkit.event.entity.ProjectileLaunchEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
@@ -73,6 +81,19 @@ public class PVPArenaManager implements Listener {
|
||||
// 已完成装备选择的玩家(备份+清空+发装备已执行完毕)
|
||||
private final Set<String> equipmentConfirmed = ConcurrentHashMap.newKeySet();
|
||||
|
||||
// ★ 永久确认玩家(选装一次即锁定,退出再进入也不能重新选装,仅调整食物/盾牌/弓弩)
|
||||
private final Set<String> permanentlyConfirmed = ConcurrentHashMap.newKeySet();
|
||||
|
||||
// ★ 永久确认玩家的选装结果(跨次保留,供调整GUI使用)
|
||||
private static class SavedSelection {
|
||||
int weaponTier, armorTier, rangedWeapon, food;
|
||||
boolean enchant, shield;
|
||||
SavedSelection(int w, int a, int r, boolean e, boolean sh, int f) {
|
||||
weaponTier=w; armorTier=a; rangedWeapon=r; enchant=e; shield=sh; food=f;
|
||||
}
|
||||
}
|
||||
private final Map<String, SavedSelection> savedSelections = new ConcurrentHashMap<>();
|
||||
|
||||
// 程序内重开装备GUI(如切换档位)时,旧GUI关闭事件需忽略,避免误触发遣返
|
||||
private final Set<String> guiReopening = ConcurrentHashMap.newKeySet();
|
||||
|
||||
@@ -110,6 +131,9 @@ public class PVPArenaManager implements Listener {
|
||||
private final Map<String, Integer> pvpKills = new ConcurrentHashMap<>();
|
||||
private final Map<String, Integer> pvpDeaths = new ConcurrentHashMap<>();
|
||||
|
||||
// ★ 当前竞技场会话ID(持久化到DB)
|
||||
private int currentSessionId = -1;
|
||||
|
||||
// 连杀播报(移植自 PVPManager,全服播报)
|
||||
private final Map<String, PVPManager.KillSession>
|
||||
arenaKillSessions =
|
||||
@@ -118,6 +142,18 @@ public class PVPArenaManager implements Listener {
|
||||
ConcurrentHashMap.newKeySet();
|
||||
private static final String ARENA_LABEL = "PVP竞技场";
|
||||
|
||||
// ★ PVP特殊道具常量
|
||||
private static final String FIRE_CHARGE_NAME = "§c§lPVP火球";
|
||||
private static final String BRIDGE_EGG_NAME = "§a§lPVP搭桥蛋";
|
||||
private static final Material BRIDGE_BLOCK = Material.GLASS;
|
||||
|
||||
// 搭桥蛋追踪:egg实体ID -> 跟随任务的ID
|
||||
private final Map<Integer, Integer> bridgeEggTrackers = new ConcurrentHashMap<>();
|
||||
// 搭桥蛋已跳过的tick数(前2tick跳过,避免生成在玩家身上)
|
||||
private final Map<Integer, Integer> bridgeEggSkipTicks = new ConcurrentHashMap<>();
|
||||
// 搭桥蛋上一tick的位置(方块放这里,避免蛋撞自己刚放的方块)
|
||||
private final Map<Integer, Location> bridgeLastLoc = new ConcurrentHashMap<>();
|
||||
|
||||
// 记分板条目缓存:玩家名 -> 侧边栏条目字符串(用于精确清除)
|
||||
private final Map<String, String> scoreEntries = new ConcurrentHashMap<>();
|
||||
|
||||
@@ -332,6 +368,11 @@ public class PVPArenaManager implements Listener {
|
||||
// 同步分批预生成出生点周边区块(半径2=5×5=25个,每 tick 一行,均摊不冻服)
|
||||
preGenerateSpawnChunksAsync(pvpWorld, 2);
|
||||
reapplyWorldRules(pvpWorld);
|
||||
// ★ 创建DB会话记录(持久化战绩)
|
||||
currentSessionId = db.createArenaSession(pvpWorldName);
|
||||
if (currentSessionId > 0) {
|
||||
plugin.getLogger().info("[PVP] 创建竞技场会话 #" + currentSessionId + " (world=" + pvpWorldName + ")");
|
||||
}
|
||||
|
||||
plugin.getLogger().info("[PVP] PVP竞技场世界就绪 种子=" + seed + " 耗时=" + dt + "ms(NORMAL真实自然地形, 出生点已固化)");
|
||||
return pvpWorld;
|
||||
@@ -389,20 +430,17 @@ public class PVPArenaManager implements Listener {
|
||||
|
||||
// 用数组持有任务引用,供 lambda 内部自取消(Bukkit 无 getCurrentTask)
|
||||
final BukkitTask[] taskHolder = new BukkitTask[1];
|
||||
taskHolder[0] = Bukkit.getScheduler().runTaskTimer(plugin, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
int r = row.getAndIncrement();
|
||||
if (r > radius) {
|
||||
taskHolder[0].cancel();
|
||||
return;
|
||||
}
|
||||
for (int dz = -radius; dz <= radius; dz++) {
|
||||
try {
|
||||
world.loadChunk(cx + r, cz + dz, true);
|
||||
} catch (Exception e) {
|
||||
// 单个 chunk 失败不阻断整体
|
||||
}
|
||||
taskHolder[0] = Bukkit.getScheduler().runTaskTimer(plugin, (Runnable) () -> {
|
||||
int r = row.getAndIncrement();
|
||||
if (r > radius) {
|
||||
taskHolder[0].cancel();
|
||||
return;
|
||||
}
|
||||
for (int dz = -radius; dz <= radius; dz++) {
|
||||
try {
|
||||
world.loadChunk(cx + r, cz + dz, true);
|
||||
} catch (Exception e) {
|
||||
// 单个 chunk 失败不阻断整体
|
||||
}
|
||||
}
|
||||
}, 1L, 1L); // 延迟 1tick 启动(给世界加载留缓冲),每 1tick 一行
|
||||
@@ -497,6 +535,8 @@ public class PVPArenaManager implements Listener {
|
||||
|
||||
private void reapplyWorldRules(World w) {
|
||||
w.setPVP(true);
|
||||
// ★ 和平模式:排除敌对生物干扰PVP
|
||||
w.setDifficulty(Difficulty.PEACEFUL);
|
||||
w.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false);
|
||||
w.setGameRule(GameRule.DO_WEATHER_CYCLE, false);
|
||||
w.setTime(6000);
|
||||
@@ -739,7 +779,7 @@ public class PVPArenaManager implements Listener {
|
||||
// 兜底:确保世界内无玩家(理论上冷却结束时已无人)
|
||||
for (Player p : new ArrayList<>(world.getPlayers())) {
|
||||
World main = Bukkit.getWorlds().get(0);
|
||||
p.teleport(main.getSpawnLocation());
|
||||
p.teleport(getBedOrSpawnLocation(p));
|
||||
p.sendMessage("§e[PVP] 竞技场已关闭,你被传回主世界");
|
||||
}
|
||||
Bukkit.unloadWorld(world, false); // false=不保存(即将删除,避免残留破坏的地形)
|
||||
@@ -917,8 +957,7 @@ public class PVPArenaManager implements Listener {
|
||||
// 延迟一帧传回主世界(避免与关闭事件冲突)
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
if (player.isOnline()) {
|
||||
World main = Bukkit.getWorlds().get(0);
|
||||
player.teleport(main.getSpawnLocation());
|
||||
player.teleport(getBedOrSpawnLocation(player));
|
||||
player.sendMessage("§c§l[PVP] §c你未确认装备选择,已被遣返回主世界");
|
||||
player.sendMessage("§7提示:进入PVP后必须选择并确认装备才能开始战斗");
|
||||
player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f);
|
||||
@@ -982,6 +1021,17 @@ public class PVPArenaManager implements Listener {
|
||||
equipmentConfirmed.add(playerName);
|
||||
cancelKickTimeout(playerName);
|
||||
|
||||
// ★ 永久锁定:记录确认状态和选装结果,退出再进入也不能重新选装
|
||||
permanentlyConfirmed.add(playerName);
|
||||
savedSelections.put(playerName, new SavedSelection(
|
||||
selWeaponTier.getOrDefault(playerName, 0),
|
||||
selArmorTier.getOrDefault(playerName, 0),
|
||||
selRangedWeapon.getOrDefault(playerName, 0),
|
||||
selEnchant.getOrDefault(playerName, false),
|
||||
selShield.getOrDefault(playerName, true),
|
||||
selFood.getOrDefault(playerName, FOOD_DEFAULT)
|
||||
));
|
||||
|
||||
player.sendMessage("§a§l装备已就绪,开始战斗!");
|
||||
player.playSound(player.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1.0f, 1.0f);
|
||||
plugin.getLogger().info("[PVP] 玩家 " + playerName + " 装备确认完毕");
|
||||
@@ -1028,8 +1078,7 @@ public class PVPArenaManager implements Listener {
|
||||
equipInteracted.remove(playerName);
|
||||
if (p != null && p.isOnline()) {
|
||||
p.closeInventory();
|
||||
World main = Bukkit.getWorlds().get(0);
|
||||
p.teleport(main.getSpawnLocation());
|
||||
p.teleport(getBedOrSpawnLocation(p));
|
||||
p.sendMessage("§c§l[PVP] §c装备选择超时(" + EQUIPMENT_SELECT_TIMEOUT_SECONDS + "秒),已被自动遣返");
|
||||
p.playSound(p.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f);
|
||||
}
|
||||
@@ -1128,6 +1177,89 @@ public class PVPArenaManager implements Listener {
|
||||
|
||||
// ==================== 事件处理器 ====================
|
||||
|
||||
/**
|
||||
* PVP火球 — 右键火焰弹发射SmallFireball,造成伤害
|
||||
*/
|
||||
@EventHandler
|
||||
public void onFireBallUse(PlayerInteractEvent event) {
|
||||
if (event.getAction() != Action.RIGHT_CLICK_AIR && event.getAction() != Action.RIGHT_CLICK_BLOCK) return;
|
||||
Player player = event.getPlayer();
|
||||
if (!inPVPArena.contains(player.getName()) || !equipmentConfirmed.contains(player.getName())) return;
|
||||
|
||||
ItemStack item = player.getInventory().getItemInMainHand();
|
||||
if (item.getType() != Material.FIRE_CHARGE) return;
|
||||
|
||||
event.setCancelled(true);
|
||||
|
||||
// 消耗1个火焰弹
|
||||
if (item.getAmount() > 1) {
|
||||
item.setAmount(item.getAmount() - 1);
|
||||
} else {
|
||||
player.getInventory().setItemInMainHand(null);
|
||||
}
|
||||
|
||||
// 发射火焰弹(落点爆炸 + 点燃方块)
|
||||
Vector dir = player.getLocation().getDirection().normalize().multiply(2.0);
|
||||
SmallFireball fb = player.launchProjectile(SmallFireball.class, dir);
|
||||
fb.setYield(1.5f); // 爆炸威力约1.5格半径
|
||||
fb.setIsIncendiary(true); // 点燃爆炸范围内方块
|
||||
player.playSound(player.getLocation(), Sound.ENTITY_BLAZE_SHOOT, 0.5f, 1.2f);
|
||||
}
|
||||
|
||||
/**
|
||||
* 搭桥蛋 — 右键扔出蛋后,每隔1tick在蛋当前位置放置方块,形成桥梁
|
||||
*/
|
||||
@EventHandler
|
||||
public void onBridgeEggLaunch(ProjectileLaunchEvent event) {
|
||||
if (!(event.getEntity() instanceof Egg egg)) return;
|
||||
if (!(egg.getShooter() instanceof Player player)) return;
|
||||
if (!inPVPArena.contains(player.getName())) return;
|
||||
|
||||
int eid = egg.getEntityId();
|
||||
int taskId = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
|
||||
if (egg.isDead() || !egg.isValid()) {
|
||||
Integer tid = bridgeEggTrackers.remove(eid);
|
||||
if (tid != null) Bukkit.getScheduler().cancelTask(tid);
|
||||
bridgeEggSkipTicks.remove(eid);
|
||||
bridgeLastLoc.remove(eid);
|
||||
return;
|
||||
}
|
||||
int skip = bridgeEggSkipTicks.merge(eid, 1, Integer::sum);
|
||||
// 前2tick只记录位置不放置(蛋在玩家体内/附近),保证 skip=3 起桥位已就绪
|
||||
if (skip <= 2) {
|
||||
bridgeLastLoc.put(eid, egg.getLocation().clone());
|
||||
return;
|
||||
}
|
||||
// 方块放在蛋的上一tick位置(蛋已离开,不会撞自己放的桥)
|
||||
Location prevLoc = bridgeLastLoc.get(eid);
|
||||
if (prevLoc != null) {
|
||||
Block b = prevLoc.getBlock();
|
||||
if (b.getType().isAir()) {
|
||||
b.setType(BRIDGE_BLOCK);
|
||||
}
|
||||
}
|
||||
// 记录当前位置供下一tick使用
|
||||
bridgeLastLoc.put(eid, egg.getLocation().clone());
|
||||
}, 1L, 1L).getTaskId();
|
||||
|
||||
bridgeEggTrackers.put(eid, taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 搭桥蛋命中 — 停止方块追踪
|
||||
*/
|
||||
@EventHandler
|
||||
public void onBridgeEggHit(ProjectileHitEvent event) {
|
||||
if (!(event.getEntity() instanceof Egg)) return;
|
||||
int eid = event.getEntity().getEntityId();
|
||||
Integer taskId = bridgeEggTrackers.remove(eid);
|
||||
if (taskId != null) {
|
||||
Bukkit.getScheduler().cancelTask(taskId);
|
||||
}
|
||||
bridgeEggSkipTicks.remove(eid);
|
||||
bridgeLastLoc.remove(eid);
|
||||
}
|
||||
|
||||
/**
|
||||
* ★ 选装前无敌:玩家在PVP世界中未完成装备选择时,免疫一切伤害
|
||||
*/
|
||||
@@ -1157,41 +1289,89 @@ public class PVPArenaManager implements Listener {
|
||||
// 总榜击杀数
|
||||
pvpKills.put(kName, pvpKills.getOrDefault(kName, 0) + 1);
|
||||
|
||||
// ★ 击杀奖励:PVP世界每完成2次杀敌,发放16个牛排用于补血(背包满则只发能放下的)
|
||||
// ★ 持久化到DB
|
||||
if (currentSessionId > 0) {
|
||||
db.recordArenaKill(currentSessionId, ((Player) killer).getUniqueId().toString(), kName);
|
||||
db.recordArenaDeath(currentSessionId, player.getUniqueId().toString(), vName);
|
||||
}
|
||||
|
||||
// ★ 击杀奖励:5杀一个轮回
|
||||
// 2杀:16熟食补血 + 1火焰弹
|
||||
// 5杀:16箭 + 1搭桥蛋 + 1火焰弹 + 替换全身装备(头盔胸甲裤子靴子)
|
||||
int newKills = pvpKills.get(kName);
|
||||
if (newKills % 2 == 0) {
|
||||
int cyclePos = newKills % 5;
|
||||
|
||||
if (cyclePos == 2 && newKills > 0) {
|
||||
// ▼ 2杀奖励 ▼
|
||||
Player killerP = (Player) killer;
|
||||
Map<Integer, ItemStack> leftover = killerP.getInventory().addItem(new ItemStack(Material.COOKED_BEEF, 16));
|
||||
int given = 16;
|
||||
for (ItemStack rem : leftover.values()) given -= rem.getAmount();
|
||||
if (given > 0) {
|
||||
killerP.sendMessage("§a§l[PVP] §f完成 2 次杀敌!获得 §6" + given + " 个牛排 §7(补血)");
|
||||
// 16熟食
|
||||
ItemStack food = new ItemStack(Material.COOKED_BEEF, 16);
|
||||
ItemMeta foodMeta = food.getItemMeta();
|
||||
if (foodMeta != null) {
|
||||
foodMeta.setDisplayName("§c§lPVP熟食补血");
|
||||
food.setItemMeta(foodMeta);
|
||||
}
|
||||
Map<Integer, ItemStack> leftover = killerP.getInventory().addItem(food);
|
||||
int givenFood = 16;
|
||||
for (ItemStack rem : leftover.values()) givenFood -= rem.getAmount();
|
||||
|
||||
// 1火焰弹
|
||||
ItemStack fc = new ItemStack(Material.FIRE_CHARGE, 1);
|
||||
ItemMeta fcMeta = fc.getItemMeta();
|
||||
if (fcMeta != null) {
|
||||
fcMeta.setDisplayName(FIRE_CHARGE_NAME);
|
||||
fc.setItemMeta(fcMeta);
|
||||
}
|
||||
killerP.getInventory().addItem(fc);
|
||||
|
||||
if (givenFood > 0) {
|
||||
killerP.sendMessage("§a§l[PVP] §f击杀达 §e2 §f次!获得 §6" + givenFood + " 个熟食§7(补血) §c+ 火焰弹§7×1");
|
||||
killerP.playSound(killerP.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 0.6f, 1.4f);
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 击杀≥10奖励:替换完整铁甲 + 喷溅瞬间治疗2 ×1(一次性)
|
||||
if (newKills >= 10) {
|
||||
if (cyclePos == 0 && newKills > 0) {
|
||||
// ▼ 5杀奖励 ▼
|
||||
Player killerP = (Player) killer;
|
||||
if (!killerP.hasPermission("sdf1.kit.pvp.reward10")) {
|
||||
// 替换完整铁甲
|
||||
killerP.getInventory().setHelmet(new ItemStack(Material.IRON_HELMET));
|
||||
killerP.getInventory().setChestplate(new ItemStack(Material.IRON_CHESTPLATE));
|
||||
killerP.getInventory().setLeggings(new ItemStack(Material.IRON_LEGGINGS));
|
||||
killerP.getInventory().setBoots(new ItemStack(Material.IRON_BOOTS));
|
||||
// 喷溅瞬间治疗2 ×1(正确NBT)
|
||||
ItemStack potion = new ItemStack(Material.SPLASH_POTION, 1);
|
||||
PotionMeta potionMeta = (PotionMeta) potion.getItemMeta();
|
||||
potionMeta.setBasePotionType(org.bukkit.potion.PotionType.HEALING);
|
||||
potionMeta.addCustomEffect(new org.bukkit.potion.PotionEffect(
|
||||
org.bukkit.potion.PotionEffectType.INSTANT_HEALTH, 1, 1, false, false), true);
|
||||
potion.setItemMeta(potionMeta);
|
||||
killerP.getInventory().addItem(potion);
|
||||
// 授予临时权限标记(本次重启内有效,防止重复发放)
|
||||
killerP.addAttachment(plugin, "sdf1.kit.pvp.reward10", true);
|
||||
killerP.sendMessage("§a§l[PVP] §f击杀达 §610 §f次!获得 §b铁甲套装 §f+ §c喷溅瞬间治疗II ×1");
|
||||
killerP.playSound(killerP.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.0f);
|
||||
|
||||
// 16箭
|
||||
ItemStack arrows = new ItemStack(Material.ARROW, 16);
|
||||
ItemMeta aMeta = arrows.getItemMeta();
|
||||
if (aMeta != null) {
|
||||
aMeta.setDisplayName("§b§lPVP箭矢");
|
||||
arrows.setItemMeta(aMeta);
|
||||
}
|
||||
killerP.getInventory().addItem(arrows);
|
||||
|
||||
// 1搭桥蛋
|
||||
ItemStack bridgeEgg = new ItemStack(Material.EGG, 1);
|
||||
ItemMeta eMeta = bridgeEgg.getItemMeta();
|
||||
if (eMeta != null) {
|
||||
eMeta.setDisplayName(BRIDGE_EGG_NAME);
|
||||
eMeta.setLore(Arrays.asList("§7右键发射,沿抛物线生成放置方块"));
|
||||
bridgeEgg.setItemMeta(eMeta);
|
||||
}
|
||||
killerP.getInventory().addItem(bridgeEgg);
|
||||
|
||||
// 1火焰弹
|
||||
ItemStack fc5 = new ItemStack(Material.FIRE_CHARGE, 1);
|
||||
ItemMeta fc5Meta = fc5.getItemMeta();
|
||||
if (fc5Meta != null) {
|
||||
fc5Meta.setDisplayName(FIRE_CHARGE_NAME);
|
||||
fc5.setItemMeta(fc5Meta);
|
||||
}
|
||||
killerP.getInventory().addItem(fc5);
|
||||
|
||||
// 替换全身装备(用玩家当前选中的护甲档位,无选中默认铁)
|
||||
int tier = selArmorTier.getOrDefault(killerP.getName(), 0);
|
||||
Material[] armorSet = ARMOR_SETS[tier];
|
||||
killerP.getInventory().setHelmet(new ItemStack(armorSet[0]));
|
||||
killerP.getInventory().setChestplate(new ItemStack(armorSet[1]));
|
||||
killerP.getInventory().setLeggings(new ItemStack(armorSet[2]));
|
||||
killerP.getInventory().setBoots(new ItemStack(armorSet[3]));
|
||||
|
||||
killerP.sendMessage("§a§l[PVP] §f击杀达 §e5 §f次!获得 §b箭矢×16 §a+ 搭桥蛋 §c+ 火焰弹 §f+ 全新" + ARMOR_TIER_NAMES[tier] + "护甲");
|
||||
killerP.playSound(killerP.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.0f);
|
||||
}
|
||||
|
||||
// 击杀者连杀会话
|
||||
@@ -1239,6 +1419,10 @@ public class PVPArenaManager implements Listener {
|
||||
((Player) killer).sendMessage("\u00a7a\u00a7l击杀 " + vName + "\u00a7a 当前击杀: " + pvpKills.get(kName));
|
||||
}
|
||||
pvpDeaths.put(player.getName(), pvpDeaths.getOrDefault(player.getName(), 0) + 1);
|
||||
// ★ 非击杀死亡也持久化(如摔死/溺水)
|
||||
if (currentSessionId > 0) {
|
||||
db.recordArenaDeath(currentSessionId, player.getUniqueId().toString(), player.getName());
|
||||
}
|
||||
refreshPVPScoreboard();
|
||||
|
||||
// 死亡时保留经验(避免丢失)
|
||||
@@ -1255,8 +1439,13 @@ public class PVPArenaManager implements Listener {
|
||||
Player player = event.getPlayer();
|
||||
if (!inPVPArena.contains(player.getName())) return;
|
||||
|
||||
World main = Bukkit.getWorlds().get(0);
|
||||
event.setRespawnLocation(main.getSpawnLocation());
|
||||
// ★ 优先床/重生点,兜底主世界出生点
|
||||
Location bed = player.getRespawnLocation();
|
||||
if (bed != null && !bed.getWorld().getName().equals(pvpWorldName)) {
|
||||
event.setRespawnLocation(bed);
|
||||
} else {
|
||||
event.setRespawnLocation(Bukkit.getWorlds().get(0).getSpawnLocation());
|
||||
}
|
||||
|
||||
if (!equipmentConfirmed.contains(player.getName())) {
|
||||
// 未确认装备:身上仍是原背包,只需清状态
|
||||
@@ -1346,8 +1535,7 @@ public class PVPArenaManager implements Listener {
|
||||
|
||||
// 若仍在竞技场世界,送回主世界
|
||||
if (player.getWorld().getName().equals(pvpWorldName)) {
|
||||
World main = Bukkit.getWorlds().get(0);
|
||||
player.teleport(main.getSpawnLocation());
|
||||
player.teleport(getBedOrSpawnLocation(player));
|
||||
cleanupPlayerStats(player);
|
||||
inPVPArena.remove(player.getName());
|
||||
player.sendMessage("§e[PVP] 已送你回主世界");
|
||||
@@ -1360,8 +1548,7 @@ public class PVPArenaManager implements Listener {
|
||||
if (player.getWorld().getName().equals(pvpWorldName)) {
|
||||
plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
|
||||
if (!player.isOnline()) return;
|
||||
World main = Bukkit.getWorlds().get(0);
|
||||
player.teleport(main.getSpawnLocation());
|
||||
player.teleport(getBedOrSpawnLocation(player));
|
||||
cleanupPlayerStats(player);
|
||||
inPVPArena.remove(player.getName());
|
||||
player.sendMessage("§e[PVP] 检测到你在竞技场非正常断线,已送你回主世界");
|
||||
@@ -1531,12 +1718,40 @@ public class PVPArenaManager implements Listener {
|
||||
* 打开装备选择GUI
|
||||
*/
|
||||
public void openEquipmentSelection(Player player) {
|
||||
// ★ 记录GUI打开时间(宽限期机制:此后短时间内内的关闭事件不触发遣返)
|
||||
guiOpenedMillis.put(player.getName(), System.currentTimeMillis());
|
||||
graceReopenCount.put(player.getName(), 0);
|
||||
|
||||
String name = player.getName();
|
||||
|
||||
// ★ 永久确认:选装一次即锁定,退出再进入也不能重新选装
|
||||
if (permanentlyConfirmed.contains(name)) {
|
||||
// 恢复之前保存的选装状态到临时map(供 equipFullSet 和 openAdjustmentGUI 读取)
|
||||
SavedSelection saved = savedSelections.get(name);
|
||||
if (saved != null) {
|
||||
selWeaponTier.put(name, saved.weaponTier);
|
||||
selArmorTier.put(name, saved.armorTier);
|
||||
selRangedWeapon.put(name, saved.rangedWeapon);
|
||||
selEnchant.put(name, saved.enchant);
|
||||
selShield.put(name, saved.shield);
|
||||
selFood.put(name, saved.food);
|
||||
}
|
||||
// ★ 关键:背包已在 onPlayerEnterPVPWorld 中清空,必须重新发放锁定装备
|
||||
equipFullSet(player);
|
||||
// 标记本次进入已确认(超时遣返不触发)
|
||||
equipmentConfirmed.add(name);
|
||||
cancelKickTimeout(name);
|
||||
openAdjustmentGUI(player);
|
||||
player.sendMessage("§a§l装备已就绪(沿用上次选择)");
|
||||
return;
|
||||
}
|
||||
|
||||
// ★ 已确认装备的玩家(同一次进入内)→ 打开调整GUI(仅食物/盾牌/弓弩,其他锁定)
|
||||
if (equipmentConfirmed.contains(name)) {
|
||||
openAdjustmentGUI(player);
|
||||
return;
|
||||
}
|
||||
|
||||
// ★ 记录GUI打开时间(宽限期机制:此后短时间内内的关闭事件不触发遣返)
|
||||
guiOpenedMillis.put(name, System.currentTimeMillis());
|
||||
graceReopenCount.put(name, 0);
|
||||
|
||||
// ★ 公平锁定:本局首位进入PVP世界的玩家定死"武器材质/护甲材质/附魔"三档,后续加入者不可更改
|
||||
boolean isFirst = (lockedWeaponOwner == null);
|
||||
if (isFirst) {
|
||||
@@ -1692,6 +1907,146 @@ public class PVPArenaManager implements Listener {
|
||||
player.openInventory(gui);
|
||||
}
|
||||
|
||||
/**
|
||||
* 已确认装备玩家的调整GUI:武器/护甲/附魔只读锁定,仅可调食物/盾牌/弓弩
|
||||
*/
|
||||
private void openAdjustmentGUI(Player player) {
|
||||
String name = player.getName();
|
||||
|
||||
int myWeapon = selWeaponTier.getOrDefault(name, 0);
|
||||
int myArmor = selArmorTier.getOrDefault(name, 0);
|
||||
boolean myEnchant = selEnchant.getOrDefault(name, false);
|
||||
boolean myShield = selShield.getOrDefault(name, true);
|
||||
int myFood = selFood.getOrDefault(name, FOOD_DEFAULT);
|
||||
int myRanged = selRangedWeapon.getOrDefault(name, 0);
|
||||
|
||||
Inventory gui = Bukkit.createInventory(null, 54, EQUIPMENT_GUI_TITLE);
|
||||
|
||||
// 顶部预览(0-7):锁定状态的装备显示
|
||||
gui.setItem(0, previewItem(SWORD_TIERS[myWeapon], "§7§l" + WEAPON_TIER_NAMES[myWeapon] + "§l剑 §c🔒", "§7已锁定·不可更改"));
|
||||
gui.setItem(1, previewItem(AXE_TIERS[myWeapon], "§7§l" + WEAPON_TIER_NAMES[myWeapon] + "§l斧 §c🔒", "§7已锁定·不可更改"));
|
||||
gui.setItem(2, previewItem(ARMOR_SETS[myArmor][0], "§7§l" + ARMOR_TIER_NAMES[myArmor] + "§l盔 §c🔒", "§7已锁定·不可更改"));
|
||||
gui.setItem(3, previewItem(ARMOR_SETS[myArmor][1], "§7§l" + ARMOR_TIER_NAMES[myArmor] + "§l胸甲 §c🔒", "§7已锁定·不可更改"));
|
||||
gui.setItem(4, previewItem(ARMOR_SETS[myArmor][2], "§7§l" + ARMOR_TIER_NAMES[myArmor] + "§l腿甲 §c🔒", "§7已锁定·不可更改"));
|
||||
gui.setItem(5, previewItem(ARMOR_SETS[myArmor][3], "§7§l" + ARMOR_TIER_NAMES[myArmor] + "§l靴 §c🔒", "§7已锁定·不可更改"));
|
||||
if (myShield) gui.setItem(6, previewItem(Material.SHIELD, "§a§l盾牌 ✓", "§7已选·点击可停用"));
|
||||
else gui.setItem(6, grayPreview("§7盾牌 ✗", "§7未选·点击可启用"));
|
||||
gui.setItem(7, previewItem(Material.COOKED_BEEF, "§c§l熟牛肉 x" + myFood, "§7可调整数量"));
|
||||
|
||||
// 信息面板(8)
|
||||
ItemStack info = new ItemStack(Material.PAPER);
|
||||
ItemMeta infoM = info.getItemMeta();
|
||||
if (infoM != null) {
|
||||
infoM.setDisplayName("§6§lPVP装备调整");
|
||||
List<String> lore = new ArrayList<>();
|
||||
lore.add("§7装备已锁定,仅可调整以下选项:");
|
||||
lore.add("");
|
||||
lore.add("§e⚡ 当前装备状态");
|
||||
lore.add("§7 主武器: " + WEAPON_TIER_NAMES[myWeapon] + "§7剑 + " + WEAPON_TIER_NAMES[myWeapon] + "§7斧");
|
||||
lore.add("§7 护甲: " + ARMOR_TIER_NAMES[myArmor] + "§7全套4件");
|
||||
lore.add("§7 附魔: " + (myEnchant ? "§a已开启" : "§c未开启"));
|
||||
lore.add("");
|
||||
lore.add("§a↕ 可调整项:");
|
||||
lore.add("§7 - 食物补给数量");
|
||||
lore.add("§7 - 盾牌启用/停用");
|
||||
lore.add("§7 - 弓/弩切换");
|
||||
infoM.setLore(lore);
|
||||
info.setItemMeta(infoM);
|
||||
}
|
||||
gui.setItem(8, info);
|
||||
|
||||
// 分隔线(36-44,跳过40)
|
||||
for (int i = 36; i < 45; i++) {
|
||||
if (i == 40) continue;
|
||||
gui.setItem(i, new ItemStack(Material.GRAY_STAINED_GLASS_PANE));
|
||||
}
|
||||
|
||||
// 分组标签:武器/护甲标为已锁定
|
||||
gui.setItem(9, labelItem("§7§l主武器材质 §c(已锁定)", "§7首次选装后不可更改"));
|
||||
gui.setItem(18, labelItem("§7§l护甲材质 §c(已锁定)", "§7首次选装后不可更改"));
|
||||
gui.setItem(27, labelItem("§6§l可调整选项", "§7点击直接切换/选择"));
|
||||
|
||||
// 武器材质(10-13):全部只读灰显
|
||||
for (int t = 0; t < 4; t++) {
|
||||
ItemStack btn;
|
||||
if (t == myWeapon) {
|
||||
btn = new ItemStack(SWORD_TIERS[t]);
|
||||
ItemMeta bm = btn.getItemMeta();
|
||||
if (bm != null) {
|
||||
bm.setDisplayName("§7" + WEAPON_TIER_NAMES[t] + "剑 §a✔");
|
||||
bm.setLore(Arrays.asList("§7已锁定"));
|
||||
btn.setItemMeta(bm);
|
||||
}
|
||||
} else {
|
||||
btn = grayPreview("§7" + WEAPON_TIER_NAMES[t] + "剑", "§7已锁定");
|
||||
}
|
||||
gui.setItem(10 + t, btn);
|
||||
}
|
||||
|
||||
// 护甲材质(19-22):全部只读灰显
|
||||
for (int t = 0; t < 4; t++) {
|
||||
ItemStack btn;
|
||||
if (t == myArmor) {
|
||||
btn = new ItemStack(ARMOR_SETS[t][0]);
|
||||
ItemMeta bm = btn.getItemMeta();
|
||||
if (bm != null) {
|
||||
bm.setDisplayName("§7" + ARMOR_TIER_NAMES[t] + "盔 §a✔");
|
||||
bm.setLore(Arrays.asList("§7已锁定"));
|
||||
btn.setItemMeta(bm);
|
||||
}
|
||||
} else {
|
||||
btn = grayPreview("§7" + ARMOR_TIER_NAMES[t] + "盔", "§7已锁定");
|
||||
}
|
||||
gui.setItem(19 + t, btn);
|
||||
}
|
||||
|
||||
// 附魔(28):只读灰显
|
||||
ItemStack enchBtn = grayPreview(
|
||||
myEnchant ? "§7附魔: 开 §c(已锁定)" : "§7附魔: 关 §c(已锁定)",
|
||||
"§7首次选装后不可更改");
|
||||
gui.setItem(28, enchBtn);
|
||||
|
||||
// 盾牌开关(30)— 可调整
|
||||
ItemStack shBtn = new ItemStack(myShield ? Material.LIME_STAINED_GLASS_PANE : Material.RED_STAINED_GLASS_PANE);
|
||||
ItemMeta sm = shBtn.getItemMeta();
|
||||
if (sm != null) {
|
||||
sm.setDisplayName(myShield ? "§a§l盾牌: 装备" : "§c§l盾牌: 不装备");
|
||||
sm.setLore(Arrays.asList("§7副手装备", myShield ? "§a点击取消" : "§a点击装备"));
|
||||
shBtn.setItemMeta(sm);
|
||||
}
|
||||
gui.setItem(30, shBtn);
|
||||
|
||||
// 远程武器切换(29)— 可调整
|
||||
ItemStack rwBtn = new ItemStack(myRanged == 1 ? Material.CROSSBOW : Material.BOW);
|
||||
ItemMeta rwm = rwBtn.getItemMeta();
|
||||
if (rwm != null) {
|
||||
rwm.setDisplayName(myRanged == 1 ? "§b§l远程武器: 弩" : "§a§l远程武器: 弓");
|
||||
rwm.setLore(Arrays.asList("§7点击在 弓 / 弩 之间切换",
|
||||
"§e当前: " + (myRanged == 1 ? "弩" : "弓"),
|
||||
"§7调整后立即生效"));
|
||||
rwBtn.setItemMeta(rwm);
|
||||
}
|
||||
gui.setItem(29, rwBtn);
|
||||
|
||||
// 熟牛肉快捷数量(32-35 = 8/16/32/64)
|
||||
int[] foodOpts = {8, 16, 32, 64};
|
||||
for (int i = 0; i < 4; i++) {
|
||||
gui.setItem(32 + i, makeFoodButton(foodOpts[i], myFood));
|
||||
}
|
||||
|
||||
// 关闭按钮(49)
|
||||
ItemStack close = new ItemStack(Material.BARRIER);
|
||||
ItemMeta cm = close.getItemMeta();
|
||||
if (cm != null) {
|
||||
cm.setDisplayName("§c§l关闭");
|
||||
cm.setLore(Arrays.asList("§7关闭装备调整界面"));
|
||||
close.setItemMeta(cm);
|
||||
}
|
||||
gui.setItem(49, close);
|
||||
|
||||
player.openInventory(gui);
|
||||
}
|
||||
|
||||
private ItemStack previewItem(Material mat, String name, String desc) {
|
||||
ItemStack it = new ItemStack(mat);
|
||||
ItemMeta m = it.getItemMeta();
|
||||
@@ -1762,6 +2117,20 @@ public class PVPArenaManager implements Listener {
|
||||
// ★ 旧5档护甲系统已废弃(2026-07-09 改版为铁剑+铁斧+可选盾牌+可选附魔+可调熟牛肉)
|
||||
// 原 buildTierEquipment(EquipmentTier) 与 makePotion 整体移除,装备由 equipFullSet 按新模型直接构建。
|
||||
|
||||
/**
|
||||
* 创建带PVP标记的箭矢堆
|
||||
*/
|
||||
private ItemStack makeArrowStack(int amount) {
|
||||
ItemStack arrows = new ItemStack(Material.ARROW, amount);
|
||||
ItemMeta m = arrows.getItemMeta();
|
||||
if (m != null) {
|
||||
m.setDisplayName("§b§lPVP箭矢");
|
||||
m.setLore(Arrays.asList("§7远程武器弹药", PVP_ITEM_MARKER));
|
||||
arrows.setItemMeta(m);
|
||||
}
|
||||
return arrows;
|
||||
}
|
||||
|
||||
private ItemStack makeWeapon(Material mat, String name, boolean enchant) {
|
||||
ItemStack item = new ItemStack(mat);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
@@ -1798,6 +2167,75 @@ public class PVPArenaManager implements Listener {
|
||||
|
||||
String name = player.getName();
|
||||
|
||||
// ★ 已确认装备 → 调整模式:仅允许调整食物/盾牌/弓弩,其余全部锁定
|
||||
boolean confirmed = equipmentConfirmed.contains(name);
|
||||
if (confirmed) {
|
||||
// 武器材质(10-13)— 锁定
|
||||
if (slot >= 10 && slot <= 13) {
|
||||
player.sendMessage("§e[PVP] §7装备已锁定,不可更改武器材质");
|
||||
player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f);
|
||||
return true;
|
||||
}
|
||||
// 护甲材质(19-22)— 锁定
|
||||
if (slot >= 19 && slot <= 22) {
|
||||
player.sendMessage("§e[PVP] §7装备已锁定,不可更改护甲材质");
|
||||
player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f);
|
||||
return true;
|
||||
}
|
||||
// 附魔(28)— 锁定
|
||||
if (slot == 28) {
|
||||
player.sendMessage("§e[PVP] §7附魔已锁定,不可更改");
|
||||
player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1.0f, 1.0f);
|
||||
return true;
|
||||
}
|
||||
// 盾牌(30)— 可调整
|
||||
if (slot == 30) {
|
||||
boolean cur = selShield.getOrDefault(name, true);
|
||||
selShield.put(name, !cur);
|
||||
reEquipAdjustments(player);
|
||||
guiReopening.add(name);
|
||||
openAdjustmentGUI(player);
|
||||
player.sendMessage("§a[PVP] 盾牌已" + (!cur ? "§a装备" : "§c取消"));
|
||||
player.playSound(player.getLocation(), Sound.UI_BUTTON_CLICK, 0.7f, 1.2f);
|
||||
return true;
|
||||
}
|
||||
// 远程武器(29)— 可调整
|
||||
if (slot == 29) {
|
||||
int cur = selRangedWeapon.getOrDefault(name, 0);
|
||||
selRangedWeapon.put(name, cur == 0 ? 1 : 0);
|
||||
reEquipAdjustments(player);
|
||||
guiReopening.add(name);
|
||||
openAdjustmentGUI(player);
|
||||
player.sendMessage("§a[PVP] 远程武器已切换为 " + (cur == 0 ? "§b弩" : "§b弓"));
|
||||
player.playSound(player.getLocation(), Sound.UI_BUTTON_CLICK, 0.7f, 1.2f);
|
||||
return true;
|
||||
}
|
||||
// 食物数量(32-35)— 可调整
|
||||
if (slot >= 32 && slot <= 35) {
|
||||
int[] foodOpts = {8, 16, 32, 64};
|
||||
int food = foodOpts[slot - 32];
|
||||
selFood.put(name, food);
|
||||
reEquipAdjustments(player);
|
||||
guiReopening.add(name);
|
||||
openAdjustmentGUI(player);
|
||||
player.sendMessage("§a[PVP] 熟牛肉数量已设为 " + food);
|
||||
player.playSound(player.getLocation(), Sound.UI_BUTTON_CLICK, 0.5f, 1.0f);
|
||||
return true;
|
||||
}
|
||||
// 关闭(49)
|
||||
if (slot == 49) {
|
||||
player.closeInventory();
|
||||
player.playSound(player.getLocation(), Sound.UI_BUTTON_CLICK, 0.5f, 1.0f);
|
||||
return true;
|
||||
}
|
||||
// 预览区/标签(0-8, 9, 18, 27):仅音效
|
||||
if ((slot >= 0 && slot <= 8) || slot == 9 || slot == 18 || slot == 27) {
|
||||
player.playSound(player.getLocation(), Sound.UI_BUTTON_CLICK, 0.5f, 1.0f);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean weaponLocked = (lockedWeaponOwner != null && !lockedWeaponOwner.equals(name));
|
||||
boolean armorLocked = (lockedArmorOwner != null && !lockedArmorOwner.equals(name));
|
||||
boolean enchantLocked = (lockedEnchantOwner != null && !lockedEnchantOwner.equals(name));
|
||||
@@ -1976,7 +2414,7 @@ public class PVPArenaManager implements Listener {
|
||||
crossbow.setItemMeta(cm);
|
||||
}
|
||||
player.getInventory().addItem(crossbow);
|
||||
player.getInventory().addItem(new ItemStack(Material.ARROW, 64));
|
||||
player.getInventory().addItem(makeArrowStack(64));
|
||||
} else {
|
||||
ItemStack bow = new ItemStack(Material.BOW);
|
||||
ItemMeta bm = bow.getItemMeta();
|
||||
@@ -1986,7 +2424,7 @@ public class PVPArenaManager implements Listener {
|
||||
bow.setItemMeta(bm);
|
||||
}
|
||||
player.getInventory().addItem(bow);
|
||||
player.getInventory().addItem(new ItemStack(Material.ARROW, 64));
|
||||
player.getInventory().addItem(makeArrowStack(64));
|
||||
}
|
||||
|
||||
// 补血普通食物:熟牛肉(数量可调)
|
||||
@@ -2007,6 +2445,94 @@ public class PVPArenaManager implements Listener {
|
||||
+ " 远程:" + (rw == 1 ? "弩" : "弓") + " 熟牛肉x" + food);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除可调整类的PVP物品(食物/弓/弩/箭/盾牌),保留武器+护甲不动
|
||||
*/
|
||||
private void removePVPAdjustables(Player player) {
|
||||
// 副手盾牌
|
||||
ItemStack off = player.getInventory().getItemInOffHand();
|
||||
if (off != null && off.getType() == Material.SHIELD) {
|
||||
player.getInventory().setItemInOffHand(null);
|
||||
}
|
||||
// 背包中带PVP标记的食物/弓/弩/箭
|
||||
for (int i = 0; i < player.getInventory().getSize(); i++) {
|
||||
ItemStack item = player.getInventory().getItem(i);
|
||||
if (item == null || !item.hasItemMeta()) continue;
|
||||
ItemMeta m = item.getItemMeta();
|
||||
if (m == null || !m.hasLore()) continue;
|
||||
List<String> lore = m.getLore();
|
||||
if (lore == null || !lore.contains(PVP_ITEM_MARKER)) continue;
|
||||
Material t = item.getType();
|
||||
if (t == Material.COOKED_BEEF || t == Material.BOW || t == Material.CROSSBOW || t == Material.ARROW) {
|
||||
player.getInventory().setItem(i, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为已确认装备的玩家重新应用食物/盾牌/远程武器调整
|
||||
* (武器材质/护甲材质/附魔已锁定,不做变动)
|
||||
*/
|
||||
private void reEquipAdjustments(Player player) {
|
||||
String name = player.getName();
|
||||
if (!equipmentConfirmed.contains(name)) return;
|
||||
|
||||
removePVPAdjustables(player);
|
||||
|
||||
boolean shield = selShield.getOrDefault(name, true);
|
||||
int food = selFood.getOrDefault(name, FOOD_DEFAULT);
|
||||
int rw = selRangedWeapon.getOrDefault(name, 0);
|
||||
|
||||
// 盾牌(副手)
|
||||
if (shield) {
|
||||
ItemStack sh = new ItemStack(Material.SHIELD);
|
||||
ItemMeta sm = sh.getItemMeta();
|
||||
if (sm != null) {
|
||||
sm.setDisplayName("§a§lPVP盾牌");
|
||||
sm.setLore(Arrays.asList("§7格挡近战攻击", PVP_ITEM_MARKER));
|
||||
sh.setItemMeta(sm);
|
||||
}
|
||||
player.getInventory().setItemInOffHand(sh);
|
||||
}
|
||||
|
||||
// 远程武器 + 箭矢
|
||||
if (rw == 1) {
|
||||
ItemStack crossbow = new ItemStack(Material.CROSSBOW);
|
||||
ItemMeta cm = crossbow.getItemMeta();
|
||||
if (cm != null) {
|
||||
cm.setDisplayName("§b§lPVP弩");
|
||||
cm.setLore(Arrays.asList("§7远程武器(默认装备)", PVP_ITEM_MARKER));
|
||||
crossbow.setItemMeta(cm);
|
||||
}
|
||||
player.getInventory().addItem(crossbow);
|
||||
} else {
|
||||
ItemStack bow = new ItemStack(Material.BOW);
|
||||
ItemMeta bm = bow.getItemMeta();
|
||||
if (bm != null) {
|
||||
bm.setDisplayName("§b§lPVP弓");
|
||||
bm.setLore(Arrays.asList("§7远程武器(默认装备)", PVP_ITEM_MARKER));
|
||||
bow.setItemMeta(bm);
|
||||
}
|
||||
player.getInventory().addItem(bow);
|
||||
}
|
||||
player.getInventory().addItem(makeArrowStack(64));
|
||||
|
||||
// 食物
|
||||
if (food > 0) {
|
||||
ItemStack beef = new ItemStack(Material.COOKED_BEEF, food);
|
||||
ItemMeta bm = beef.getItemMeta();
|
||||
if (bm != null) {
|
||||
bm.setDisplayName("§c§lPVP熟牛肉");
|
||||
bm.setLore(Arrays.asList("§7补血普通食物 x" + food, PVP_ITEM_MARKER));
|
||||
beef.setItemMeta(bm);
|
||||
}
|
||||
player.getInventory().addItem(beef);
|
||||
}
|
||||
|
||||
player.sendMessage("§a§l[PVP] §7装备调整已应用");
|
||||
player.playSound(player.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 0.6f, 1.2f);
|
||||
}
|
||||
|
||||
// ==================== 命令入口 ====================
|
||||
|
||||
/**
|
||||
@@ -2118,9 +2644,8 @@ public class PVPArenaManager implements Listener {
|
||||
player.sendMessage("§e你不在PVP竞技场中");
|
||||
return;
|
||||
}
|
||||
// 传回主世界(触发 PlayerChangedWorldEvent → forceLeaveArena)
|
||||
World main = Bukkit.getWorlds().get(0);
|
||||
player.teleport(main.getSpawnLocation());
|
||||
// 传回主世界(触发 PlayerChangedWorldEvent → forceLeaveArena),优先床
|
||||
player.teleport(getBedOrSpawnLocation(player));
|
||||
player.sendMessage("§a§l正在离开PVP竞技场...");
|
||||
}
|
||||
|
||||
@@ -2153,6 +2678,33 @@ public class PVPArenaManager implements Listener {
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
/** 获取当前会话ID */
|
||||
public int getCurrentSessionId() {
|
||||
return currentSessionId;
|
||||
}
|
||||
|
||||
/** 获取某玩家所有会话的总战绩(从DB) */
|
||||
public Map<String, Object> getTotalArenaStats(String uuid) {
|
||||
return db.getArenaTotalStats(uuid);
|
||||
}
|
||||
|
||||
/** 获取指定会话编号的信息 */
|
||||
public Map<String, Object> getArenaSessionByNumber(int number) {
|
||||
return db.getArenaSessionByNumber(number);
|
||||
}
|
||||
|
||||
/** 获取某玩家在指定会话中的战绩 */
|
||||
public Map<String, Object> getArenaSessionStats(String uuid, int sessionId) {
|
||||
return db.getArenaSessionStats(uuid, sessionId);
|
||||
}
|
||||
|
||||
/** 优先传送玩家到床/重生点,兜底世界出生点 */
|
||||
public Location getBedOrSpawnLocation(Player player) {
|
||||
Location bed = player.getRespawnLocation();
|
||||
if (bed != null) return bed;
|
||||
return Bukkit.getWorlds().get(0).getSpawnLocation();
|
||||
}
|
||||
|
||||
/** 重置本局公平锁定状态(世界销毁/全量重置时调用,下一局首位玩家重新定死装备) */
|
||||
private void resetMatchLock() {
|
||||
lockedEnchantOwner = null;
|
||||
@@ -2168,6 +2720,8 @@ public class PVPArenaManager implements Listener {
|
||||
selRangedWeapon.clear();
|
||||
selArmorTier.clear();
|
||||
equipInteracted.clear();
|
||||
permanentlyConfirmed.clear();
|
||||
savedSelections.clear();
|
||||
}
|
||||
|
||||
public void clearAllPVPArenaStates() {
|
||||
|
||||
@@ -1076,8 +1076,15 @@ public class PVPManager implements Listener {
|
||||
public boolean onCommand(Player p,
|
||||
String[] args) {
|
||||
if (args.length == 0) {
|
||||
return showStats(p,
|
||||
p.getName());
|
||||
showStats(p, p.getName());
|
||||
// ★ 同时展示竞技场总战绩
|
||||
showArenaTotalStats(p, p.getName());
|
||||
return true;
|
||||
}
|
||||
// ★ 数字参数 → 查询指定会话编号的竞技场战绩
|
||||
if (args.length == 1 && args[0].matches("\\d+")) {
|
||||
showArenaSessionStats(p, p.getName(), Integer.parseInt(args[0]));
|
||||
return true;
|
||||
}
|
||||
switch (args[0].toLowerCase()) {
|
||||
case "create":
|
||||
@@ -1118,6 +1125,14 @@ public class PVPManager implements Listener {
|
||||
case "arena":
|
||||
plugin.getPVPArenaManager().joinArena(p);
|
||||
return true;
|
||||
case "equip":
|
||||
case "adjust":
|
||||
if (!plugin.getPVPArenaManager().isInPVPArena(p.getName())) {
|
||||
p.sendMessage("§c你不在PVP竞技场中");
|
||||
return true;
|
||||
}
|
||||
plugin.getPVPArenaManager().openEquipmentSelection(p);
|
||||
return true;
|
||||
case "leave":
|
||||
case "exit":
|
||||
plugin.leavePVP(p);
|
||||
@@ -1126,6 +1141,7 @@ public class PVPManager implements Listener {
|
||||
showStats(p, p.getName());
|
||||
p.sendMessage("§7——— PVP指令 ———");
|
||||
p.sendMessage("§e/pvp join §7进入PVP竞技场");
|
||||
p.sendMessage("§e/pvp equip §7调整装备(食物/盾牌/弓弩)");
|
||||
p.sendMessage("§e/pvp leave §7离开PVP竞技场");
|
||||
p.sendMessage("§e/pvp stats [玩家] §7查看战绩");
|
||||
p.sendMessage("§e/pvp list §7查看PVP区域列表");
|
||||
@@ -1426,4 +1442,43 @@ public class PVPManager implements Listener {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ★ 展示PVP竞技场总战绩
|
||||
private void showArenaTotalStats(Player p, String target) {
|
||||
org.bukkit.OfflinePlayer off = Bukkit.getOfflinePlayer(target);
|
||||
String uuid = off.getUniqueId().toString();
|
||||
Map<String, Object> stats = plugin.getPVPArenaManager().getTotalArenaStats(uuid);
|
||||
int kills = (int) stats.get("kills");
|
||||
int deaths = (int) stats.get("deaths");
|
||||
if (kills == 0 && deaths == 0) return;
|
||||
double kd = deaths > 0 ? (double) kills / deaths : kills;
|
||||
p.sendMessage("§6§l[PVP竞技场] " + target);
|
||||
p.sendMessage(" §a击杀: " + kills + " §c死亡: " + deaths + " §eKD: " + String.format("%.2f", kd));
|
||||
}
|
||||
|
||||
// ★ 展示指定会话编号的竞技场战绩
|
||||
private void showArenaSessionStats(Player p, String target, int sessionNumber) {
|
||||
org.bukkit.OfflinePlayer off = Bukkit.getOfflinePlayer(target);
|
||||
String uuid = off.getUniqueId().toString();
|
||||
// 先查会话信息(含3天有效期检查)
|
||||
Map<String, Object> session = plugin.getPVPArenaManager().getArenaSessionByNumber(sessionNumber);
|
||||
if (session == null) {
|
||||
p.sendMessage("§c未找到编号 " + sessionNumber + " 的竞技场记录,或已超出3天查询范围");
|
||||
return;
|
||||
}
|
||||
long createdAt = (Long) session.get("created_at");
|
||||
long threeDaysMs = 3L * 24 * 60 * 60 * 1000;
|
||||
if (System.currentTimeMillis() - createdAt > threeDaysMs) {
|
||||
p.sendMessage("§c该记录已超出3天查询范围,不再支持查询");
|
||||
return;
|
||||
}
|
||||
int dbSessionId = (int) session.get("id");
|
||||
Map<String, Object> stats = plugin.getPVPArenaManager().getArenaSessionStats(uuid, dbSessionId);
|
||||
int kills = (int) stats.get("kills");
|
||||
int deaths = (int) stats.get("deaths");
|
||||
String worldName = (String) session.get("world_name");
|
||||
String dateStr = new java.text.SimpleDateFormat("yyyy-MM-dd").format(new java.util.Date(createdAt));
|
||||
p.sendMessage("§6§l[PVP竞技场#" + sessionNumber + "] " + target + " §7(" + dateStr + " " + worldName + ")");
|
||||
p.sendMessage(" §a击杀: " + kills + " §c死亡: " + deaths + " §eKD: " + String.format("%.2f", deaths > 0 ? (double) kills / deaths : kills));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.BanList;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 风控管理器:检测异常登录行为,记录尝试次数,触发安全报警
|
||||
* 风控管理器:密码错误阈值封禁 + 报警邮件
|
||||
*
|
||||
* 功能1:密码错误达到阈值 → 自动封禁账号N分钟 + 推送报警邮件
|
||||
* 功能2:封禁期间拒绝所有登录尝试
|
||||
*/
|
||||
public class RiskControlManager {
|
||||
|
||||
@@ -17,232 +22,261 @@ public class RiskControlManager {
|
||||
private final Map<String, Integer> failAttempts =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
// 每个IP -> 关联的可疑玩家列表
|
||||
private final Map<String, Set<String>> ipSuspicious =
|
||||
// 每个玩家名 -> 封禁到期时间戳(毫秒)
|
||||
private final Map<String, Long> banUntil =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
// 每个玩家 -> 上次验证时间(用于冷却)
|
||||
private final Map<String, Long> verifyCooldown =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
// 配置项
|
||||
private static final int MAX_FAIL_ATTEMPTS = 5;
|
||||
private static final int MAX_ACCOUNTS_PER_IP = 5;
|
||||
private static final long COOLDOWN_MS = 60000L;
|
||||
|
||||
public RiskControlManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录一次失败的登录尝试
|
||||
*
|
||||
* @param playerName 玩家名
|
||||
* @return 剩余尝试次数
|
||||
* 获取当前密码错误阈值(从配置读取)
|
||||
*/
|
||||
public int recordFailAttempt(String playerName) {
|
||||
int current = failAttempts
|
||||
.getOrDefault(playerName, 0) + 1;
|
||||
failAttempts.put(playerName, current);
|
||||
return Math.max(0, MAX_FAIL_ATTEMPTS - current);
|
||||
private int getMaxFailAttempts() {
|
||||
return plugin.getConfig2().passwordFailThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置指定玩家的失败计数(登录成功时调用)
|
||||
* 获取封禁时长(毫秒)(从配置读取)
|
||||
*/
|
||||
public void resetFailAttempts(String playerName) {
|
||||
private long getBanDurationMs() {
|
||||
return (long) plugin.getConfig2().passwordBanDuration * 1000L;
|
||||
}
|
||||
|
||||
// ==================== 封禁检查 ====================
|
||||
|
||||
/**
|
||||
* 检查玩家是否已被封禁
|
||||
* @return true=已被封禁(应拒绝登录)
|
||||
*/
|
||||
public boolean isBanned(String playerName) {
|
||||
Long until = banUntil.get(playerName);
|
||||
if (until == null) return false;
|
||||
if (System.currentTimeMillis() < until) {
|
||||
return true; // 仍在封禁期内
|
||||
}
|
||||
// 封禁已过期,自动解除
|
||||
banUntil.remove(playerName);
|
||||
failAttempts.remove(playerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定玩家的失败次数
|
||||
*/
|
||||
public int getFailAttempts(String playerName) {
|
||||
return failAttempts
|
||||
.getOrDefault(playerName, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断玩家是否已被锁定(失败次数达到上限)
|
||||
*/
|
||||
public boolean isLocked(String playerName) {
|
||||
return failAttempts
|
||||
.getOrDefault(playerName, 0)
|
||||
>= MAX_FAIL_ATTEMPTS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查IP是否达到注册上限
|
||||
* 直接查数据库,不依赖 DatabaseManager 的方法
|
||||
*/
|
||||
public boolean canRegisterFromIP(String ip) {
|
||||
if (ip == null || ip.isEmpty()) return true;
|
||||
try {
|
||||
java.sql.Connection conn =
|
||||
plugin.getDb().getDb();
|
||||
if (conn == null) return true;
|
||||
java.sql.PreparedStatement ps =
|
||||
conn.prepareStatement(
|
||||
"SELECT COUNT(*) FROM users "
|
||||
+ "WHERE ip_address = ?");
|
||||
ps.setString(1, ip);
|
||||
java.sql.ResultSet rs = ps.executeQuery();
|
||||
int count = 0;
|
||||
if (rs.next()) count = rs.getInt(1);
|
||||
rs.close();
|
||||
ps.close();
|
||||
return count < 5;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 记录IP关联的可疑行为
|
||||
*/
|
||||
public void recordSuspiciousIP(String ip,
|
||||
String playerName) {
|
||||
if (ip == null || ip.isEmpty()) return;
|
||||
ipSuspicious
|
||||
.computeIfAbsent(ip,
|
||||
k -> ConcurrentHashMap.newKeySet())
|
||||
.add(playerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取某IP关联的所有可疑玩家
|
||||
*/
|
||||
public Set<String> getSuspiciousAccounts(String ip) {
|
||||
return ipSuspicious
|
||||
.getOrDefault(ip, Collections.emptySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否在验证冷却期内
|
||||
*/
|
||||
public boolean isOnCooldown(String playerName) {
|
||||
Long last = verifyCooldown.get(playerName);
|
||||
if (last == null) return false;
|
||||
return System.currentTimeMillis() - last
|
||||
< COOLDOWN_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置验证冷却时间
|
||||
*/
|
||||
public void setCooldown(String playerName) {
|
||||
verifyCooldown.put(playerName,
|
||||
System.currentTimeMillis());
|
||||
}
|
||||
|
||||
/**
|
||||
* 当检测到可疑登录时触发报警
|
||||
* - 全服广播
|
||||
* - 有邮箱则发邮件
|
||||
* - 记录到数据库
|
||||
*/
|
||||
public void triggerAlert(Player player,
|
||||
String suspiciousIP) {
|
||||
String name = player.getName();
|
||||
|
||||
// 记录到数据库
|
||||
plugin.getDb().recordSecurityAlert(
|
||||
name, suspiciousIP);
|
||||
|
||||
// 记录IP可疑行为
|
||||
recordSuspiciousIP(suspiciousIP, name);
|
||||
|
||||
// 全服广播
|
||||
String serverMsg = "§c§l[安全警告] §e" + name
|
||||
+ " §f的账号疑似遭到 §c" + suspiciousIP
|
||||
+ " §f的盗号尝试, 请各位玩家注意账号安全!";
|
||||
Bukkit.broadcastMessage(serverMsg);
|
||||
|
||||
// 有邮箱则发邮件
|
||||
String emailAddr = (String) plugin.getDb()
|
||||
.getField(name, "email");
|
||||
if (emailAddr != null
|
||||
&& !emailAddr.isEmpty()) {
|
||||
String subject =
|
||||
"[安全警告] 您的账号疑似遭到盗号尝试";
|
||||
String body = "尊敬的玩家 " + name
|
||||
+ ":\n\n"
|
||||
+ "您的账号在 "
|
||||
+ new java.text.SimpleDateFormat(
|
||||
"yyyy-MM-dd HH:mm:ss")
|
||||
.format(new java.util.Date())
|
||||
+ " 疑似遭到 IP: " + suspiciousIP
|
||||
+ " 的盗号尝试。\n\n"
|
||||
+ "建议您:\n"
|
||||
+ "1. 立即修改密码"
|
||||
+ "(使用 /sdf1_login pw)\n"
|
||||
+ "2. 如非本人操作,"
|
||||
+ "请联系管理员处理\n\n"
|
||||
+ "--Sdf1_login 安全系统";
|
||||
plugin.getEmail().sendTempPassword(
|
||||
emailAddr, name, body);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理登录失败时的风控逻辑
|
||||
*
|
||||
* @param player 玩家
|
||||
* @param suspiciousIP 可疑来源IP
|
||||
* @return true=已被锁定(应踢出), false=仍可继续尝试
|
||||
*/
|
||||
public boolean handleLoginFailure(Player player,
|
||||
String suspiciousIP) {
|
||||
String name = player.getName();
|
||||
int remaining = recordFailAttempt(name);
|
||||
|
||||
if (remaining <= 0) {
|
||||
// 达到上限,触发报警
|
||||
triggerAlert(player, suspiciousIP);
|
||||
player.kickPlayer(
|
||||
"§c§l[风控] §f登录失败次数过多,"
|
||||
+ " 账号已被临时锁定, 请联系管理员");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 未达上限,提示剩余次数
|
||||
player.sendMessage(
|
||||
"§c§l[风控] §f密码错误! "
|
||||
+ "剩余" + remaining + "次机会");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取玩家剩余封禁时间(秒),0=未封禁
|
||||
*/
|
||||
public int getBanRemainingSeconds(String playerName) {
|
||||
Long until = banUntil.get(playerName);
|
||||
if (until == null) return 0;
|
||||
long remaining = until - System.currentTimeMillis();
|
||||
if (remaining <= 0) {
|
||||
banUntil.remove(playerName);
|
||||
failAttempts.remove(playerName);
|
||||
return 0;
|
||||
}
|
||||
return (int) (remaining / 1000) + 1; // 向上取整
|
||||
}
|
||||
|
||||
// ==================== 失败记录 ====================
|
||||
|
||||
/**
|
||||
* 记录一次失败的登录尝试,判断是否触发封禁
|
||||
*
|
||||
* @param player 登录失败的玩家
|
||||
* @return 剩余尝试次数(-1=已被封禁)
|
||||
*/
|
||||
public int recordFailAttempt(Player player) {
|
||||
String name = player.getName();
|
||||
int maxAttempts = getMaxFailAttempts();
|
||||
|
||||
// 已被封禁,直接返回
|
||||
if (isBanned(name)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int current = failAttempts
|
||||
.getOrDefault(name, 0) + 1;
|
||||
failAttempts.put(name, current);
|
||||
|
||||
int remaining = Math.max(0, maxAttempts - current);
|
||||
|
||||
// 达到阈值 → 触发封禁
|
||||
if (current >= maxAttempts) {
|
||||
triggerBan(player);
|
||||
}
|
||||
|
||||
return remaining;
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发封禁:Bukkit真实封禁 + 记录时间 + 发邮件 + 踢出
|
||||
* 双层封禁:Bukkit ban阻止连接加入,内存计时器到期自动解封
|
||||
*/
|
||||
private void triggerBan(Player player) {
|
||||
String name = player.getName();
|
||||
long durationMs = getBanDurationMs();
|
||||
long until = System.currentTimeMillis() + durationMs;
|
||||
banUntil.put(name, until);
|
||||
|
||||
String ip = plugin.getPlayerIP(player);
|
||||
int banMinutes = (int) (durationMs / 60000);
|
||||
|
||||
plugin.getLogger().warning(
|
||||
"[风控] 玩家 " + name
|
||||
+ " 密码错误达到阈值("
|
||||
+ getMaxFailAttempts() + "次)"
|
||||
+ ",封禁 " + banMinutes + " 分钟");
|
||||
|
||||
// ★ 真实封禁:调用Bukkit BanList API阻止玩家重新加入服务器
|
||||
String banReason = "§c§l[安全风控] 密码连续错误"
|
||||
+ getMaxFailAttempts() + "次,"
|
||||
+ "账号临时封禁" + banMinutes + "分钟";
|
||||
try {
|
||||
// 使用 BanList API 设置带过期时间的封禁
|
||||
Date expiry = new Date(System.currentTimeMillis() + durationMs);
|
||||
Bukkit.getBanList(BanList.Type.NAME).addBan(
|
||||
name, banReason, expiry, "[Sdf1_login] 风控");
|
||||
plugin.getLogger().info(
|
||||
"[风控] 已通过Bukkit BanList封禁玩家 "
|
||||
+ name + " (" + banMinutes + "分钟,到期: " + expiry + ")");
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[风控] BanList封禁失败: " + e.getMessage());
|
||||
}
|
||||
// 踢出玩家(无论BanList是否成功,立即踢出)
|
||||
player.kickPlayer(banReason);
|
||||
|
||||
// 异步发送报警邮件
|
||||
sendBanAlertEmail(name, ip, banMinutes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送封禁报警邮件到玩家绑定的邮箱
|
||||
* 未绑定邮箱则跳过
|
||||
*/
|
||||
private void sendBanAlertEmail(String playerName,
|
||||
String ip, int banMinutes) {
|
||||
String emailAddr = (String) plugin.getDb()
|
||||
.getField(playerName, "email");
|
||||
if (emailAddr == null || emailAddr.isEmpty()) {
|
||||
plugin.getLogger().info(
|
||||
"[风控] 玩家 " + playerName
|
||||
+ " 未绑定邮箱,跳过报警邮件");
|
||||
return;
|
||||
}
|
||||
|
||||
String subject =
|
||||
"[安全警告] 您的账号已被临时封禁";
|
||||
String body = "尊敬的玩家 " + playerName + ":\n\n"
|
||||
+ "您的账号在 "
|
||||
+ new java.text.SimpleDateFormat(
|
||||
"yyyy-MM-dd HH:mm:ss")
|
||||
.format(new java.util.Date())
|
||||
+ " 因密码连续错误 "
|
||||
+ getMaxFailAttempts() + " 次"
|
||||
+ ",已被自动封禁 " + banMinutes + " 分钟。\n\n"
|
||||
+ "来源IP: " + (ip != null ? ip : "未知") + "\n"
|
||||
+ "封禁到期: "
|
||||
+ new java.text.SimpleDateFormat(
|
||||
"yyyy-MM-dd HH:mm:ss")
|
||||
.format(new java.util.Date(
|
||||
System.currentTimeMillis()
|
||||
+ (long) banMinutes * 60000L))
|
||||
+ "\n\n"
|
||||
+ "如果这是您本人操作,请等待封禁到期后重试。\n"
|
||||
+ "如果非本人操作,建议您立即修改密码"
|
||||
+ "(使用 /sdf1_login pw)。\n\n"
|
||||
+ "--Sdf1_login 安全系统";
|
||||
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
try {
|
||||
plugin.getEmail().sendBody(
|
||||
emailAddr, subject, body);
|
||||
plugin.getLogger().info(
|
||||
"[风控] 封禁报警邮件已发送到 "
|
||||
+ emailAddr);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[风控] 报警邮件发送失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 成功重置 ====================
|
||||
|
||||
/**
|
||||
* 登录成功后重置风控状态
|
||||
*/
|
||||
public void onLoginSuccess(String playerName) {
|
||||
resetFailAttempts(playerName);
|
||||
failAttempts.remove(playerName);
|
||||
banUntil.remove(playerName);
|
||||
}
|
||||
|
||||
// ==================== 清理 ====================
|
||||
|
||||
/**
|
||||
* 清理过期的冷却记录(可由定时任务调用)
|
||||
* 清理过期的封禁记录(可由定时任务调用)
|
||||
* 同步解除过期玩家的Bukkit封禁
|
||||
*/
|
||||
public void cleanup() {
|
||||
long now = System.currentTimeMillis();
|
||||
verifyCooldown.entrySet().removeIf(
|
||||
e -> now - e.getValue() > COOLDOWN_MS * 10);
|
||||
// 收集过期的玩家名
|
||||
List<String> expired = new ArrayList<>();
|
||||
banUntil.entrySet().removeIf(e -> {
|
||||
if (e.getValue() < now) {
|
||||
expired.add(e.getKey());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
// 同步解除Bukkit封禁
|
||||
for (String name : expired) {
|
||||
try {
|
||||
Bukkit.getBanList(BanList.Type.NAME)
|
||||
.pardon(name);
|
||||
plugin.getLogger().info(
|
||||
"[风控] 封禁到期,已解除Bukkit封禁: "
|
||||
+ name);
|
||||
} catch (Exception ignored) {}
|
||||
failAttempts.remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有玩家的失败尝试记录(调试用)
|
||||
* 获取所有封禁记录(调试/管理用)
|
||||
*/
|
||||
public Map<String, Long> getAllBans() {
|
||||
return Collections.unmodifiableMap(banUntil);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有失败尝试记录(调试用)
|
||||
*/
|
||||
public Map<String, Integer> getAllFailAttempts() {
|
||||
return Collections.unmodifiableMap(failAttempts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动解除某玩家的封禁(管理员用)
|
||||
*/
|
||||
public void unban(String playerName) {
|
||||
banUntil.remove(playerName);
|
||||
failAttempts.remove(playerName);
|
||||
// 同步解除Bukkit封禁
|
||||
try {
|
||||
Bukkit.getBanList(BanList.Type.NAME)
|
||||
.pardon(playerName);
|
||||
plugin.getLogger().info("[风控] 已解除Bukkit封禁: "
|
||||
+ playerName);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有风控数据(管理员调试用)
|
||||
*/
|
||||
public void clearAll() {
|
||||
failAttempts.clear();
|
||||
ipSuspicious.clear();
|
||||
verifyCooldown.clear();
|
||||
banUntil.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,6 +310,7 @@ public class WebManager {
|
||||
dbTaskQueue.offer(task);
|
||||
if (dbTaskQueue.size() > 50) {
|
||||
plugin.getLogger().warning("[DB队列] 队列积压: " + dbTaskQueue.size() + " 任务等待");
|
||||
discardOldestIfOverflow();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,6 +324,50 @@ public class WebManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ★ 清空DB队列中所有未回应的web同步任务
|
||||
* (清空队列/cleartake 命令调用)
|
||||
*/
|
||||
public void clearQueue() {
|
||||
int count = 0;
|
||||
Iterator<DbTask> it = dbTaskQueue.iterator();
|
||||
while (it.hasNext()) {
|
||||
DbTask t = it.next();
|
||||
// 只保留登录/商店等关键任务,丢弃所有同步类web任务
|
||||
if (t.name.contains("TimerA") || t.name.contains("首次")
|
||||
|| t.name.contains("周期") || t.name.contains("sync")
|
||||
|| t.name.contains("poll") || t.name.contains("push")
|
||||
|| t.name.contains("pull") || t.name.contains("cdk")) {
|
||||
it.remove();
|
||||
count++;
|
||||
}
|
||||
}
|
||||
plugin.getLogger().info("[DB队列] 清空队列: 丢弃 " + count + " 个未回应web任务");
|
||||
}
|
||||
|
||||
/**
|
||||
* ★ 队列积压时丢弃最早的任务(优先丢弃同步类低优先级任务)
|
||||
*/
|
||||
public void discardOldestIfOverflow() {
|
||||
if (dbTaskQueue.size() <= 50) return;
|
||||
int before = dbTaskQueue.size();
|
||||
// 收集所有普通同步任务(priority >= NORMAL_PRIORITY),按创建时间排序
|
||||
List<DbTask> syncTasks = new ArrayList<>();
|
||||
for (DbTask t : dbTaskQueue) {
|
||||
if (t != null && t.priority >= NORMAL_PRIORITY) {
|
||||
syncTasks.add(t);
|
||||
}
|
||||
}
|
||||
syncTasks.sort(Comparator.comparingLong(t -> t.createdAt));
|
||||
// 丢弃最早的一半同步任务(至少丢弃1个)
|
||||
int toDrop = Math.max(1, syncTasks.size() / 2);
|
||||
for (int i = 0; i < toDrop && i < syncTasks.size(); i++) {
|
||||
dbTaskQueue.remove(syncTasks.get(i));
|
||||
}
|
||||
int after = dbTaskQueue.size();
|
||||
plugin.getLogger().warning("[DB队列] 队列积压(" + before + "),自动丢弃" + (before - after) + "个最早的同步请求");
|
||||
}
|
||||
|
||||
// ==================== SSL断路器 + 降级HTTP ====================
|
||||
|
||||
/**
|
||||
@@ -872,13 +917,72 @@ public class WebManager {
|
||||
// ★ 注册交易监听器(需在BondManager初始化后调用)
|
||||
public void registerTransactionListener(BondManager bondMgr) {
|
||||
if (bondMgr == null) return;
|
||||
bondMgr.addTransactionListener((playerName, type, amount) -> {
|
||||
plugin.getLogger().info("[Web交易即时] 检测到交易: " + playerName + " " + type + " " + amount + ",立即推送");
|
||||
requestImmediateTransactionSync();
|
||||
bondMgr.addTransactionListener((playerName, type, amount, targetPlayer) -> {
|
||||
// 高频出售商品缓冲逻辑:shop_sell类型商品在1分钟内≥10次时暂缓推送
|
||||
if ("shop_sell".equals(type) && targetPlayer != null && !targetPlayer.isEmpty()) {
|
||||
handleHighFreqSell(playerName, targetPlayer, amount);
|
||||
} else {
|
||||
requestImmediateTransactionSync();
|
||||
}
|
||||
});
|
||||
plugin.getLogger().info("[Web通信] 交易即时推送监听器已注册");
|
||||
}
|
||||
|
||||
/**
|
||||
* 高频出售缓冲处理:追踪每种商品的出售频率
|
||||
* 1分钟内单种商品售卖≥10次时进入缓冲模式,暂缓推送到PHP,60秒后批量推送
|
||||
*/
|
||||
private void handleHighFreqSell(String playerName, String itemId, int amount) {
|
||||
long now = System.currentTimeMillis();
|
||||
SellItemBuffer buf = sellBuffers.computeIfAbsent(itemId, k -> new SellItemBuffer(itemId));
|
||||
|
||||
// 如果窗口已过期(>60秒),重置计数器
|
||||
if (now - buf.windowStart > 60000) {
|
||||
buf.count = 0;
|
||||
buf.windowStart = now;
|
||||
}
|
||||
|
||||
buf.count++;
|
||||
|
||||
// 已经在缓冲模式中,不做任何推送
|
||||
if (buf.buffering && now < buf.bufferEnd) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 达到阈值,进入缓冲模式
|
||||
if (buf.count >= HIGH_FREQ_SELL_THRESHOLD && !buf.buffering) {
|
||||
buf.buffering = true;
|
||||
buf.bufferEnd = now + HIGH_FREQ_BUFFER_MS;
|
||||
inHighFreqBufferMode = true;
|
||||
bufferModeEndTime = Math.max(bufferModeEndTime, buf.bufferEnd);
|
||||
plugin.getLogger().info("[Web高频] 触发缓冲: " + itemId + " " + buf.count + "次/分钟,暂缓推送60秒");
|
||||
|
||||
// 调度60秒后的批量推送
|
||||
new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
buf.buffering = false;
|
||||
buf.count = 0;
|
||||
buf.windowStart = System.currentTimeMillis();
|
||||
|
||||
// 检查是否所有缓冲都结束了
|
||||
boolean anyBuffering = false;
|
||||
for (SellItemBuffer b : sellBuffers.values()) {
|
||||
if (b.buffering) { anyBuffering = true; break; }
|
||||
}
|
||||
if (!anyBuffering) inHighFreqBufferMode = false;
|
||||
|
||||
plugin.getLogger().info("[Web高频] 缓冲到期,批量推送: " + itemId);
|
||||
requestImmediateTransactionSync();
|
||||
}
|
||||
}.runTaskLater(plugin, 20L * 60); // 60秒
|
||||
return;
|
||||
}
|
||||
|
||||
// 未达阈值或非shop_sell,立即推送
|
||||
requestImmediateTransactionSync();
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (!enabled) {
|
||||
plugin.getLogger().info("[Web通信] 未启用,在插件设置.txt中设置 web通信-启用=true");
|
||||
@@ -1318,6 +1422,26 @@ public class WebManager {
|
||||
private static final long MIN_IMMEDIATE_TX_SYNC_MS = 3000; // 最小间隔3秒,防止频繁推送
|
||||
private volatile boolean pendingImmediateTxSync = false;
|
||||
|
||||
// ★ 高频出售商品缓冲机制:1分钟内单种商品售卖≥10次时暂缓推送
|
||||
private final ConcurrentHashMap<String, SellItemBuffer> sellBuffers = new ConcurrentHashMap<>();
|
||||
private static final int HIGH_FREQ_SELL_THRESHOLD = 10; // 10次触发缓冲
|
||||
private static final long HIGH_FREQ_BUFFER_MS = 60000; // 缓冲60秒
|
||||
private volatile boolean inHighFreqBufferMode = false; // 全局缓冲模式标志
|
||||
private volatile long bufferModeEndTime = 0; // 缓冲模式结束时间
|
||||
|
||||
private static class SellItemBuffer {
|
||||
final String itemId;
|
||||
volatile int count = 0;
|
||||
volatile long windowStart = 0;
|
||||
volatile boolean buffering = false;
|
||||
volatile long bufferEnd = 0;
|
||||
|
||||
SellItemBuffer(String itemId) {
|
||||
this.itemId = itemId;
|
||||
this.windowStart = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
private void requestImmediateTransactionSync() {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastImmediateTxSyncTime < MIN_IMMEDIATE_TX_SYNC_MS) {
|
||||
@@ -4674,6 +4798,11 @@ public class WebManager {
|
||||
public void syncBondTransactions() {
|
||||
if (!enabled) return;
|
||||
|
||||
// ★ 高频出售缓冲模式:跳过周期推送,等待缓冲期满后由监听器统一推送
|
||||
if (inHighFreqBufferMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
BondManager bondMgr = plugin.getBonds();
|
||||
if (bondMgr == null) return;
|
||||
@@ -5100,6 +5229,12 @@ public class WebManager {
|
||||
// 记录为已验证正版玩家
|
||||
plugin.addVerifiedPremiumPlayer(playerName, mcUuid, mcUsername);
|
||||
|
||||
// ★ 记录OAuth登录IP(用于异地登录风控)
|
||||
String oauthIP = plugin.getPlayerIP(player);
|
||||
if (oauthIP != null && !oauthIP.isEmpty()) {
|
||||
plugin.getDb().setField(playerName, "last_oauth_ip", oauthIP);
|
||||
}
|
||||
|
||||
// 在主线程执行自动登录
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
if (player.isOnline() && !plugin.getLoggedIn().contains(playerName)) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: Sdf1_login
|
||||
version: 2.8
|
||||
version: 2.81
|
||||
main: Sdf1_login.Main
|
||||
api-version: '1.21'
|
||||
commands:
|
||||
@@ -178,3 +178,9 @@ commands:
|
||||
who:
|
||||
description: 查看在线玩家列表
|
||||
usage: /who
|
||||
清空队列:
|
||||
description: 清空DB队列中未回应的web同步任务(管理员)
|
||||
usage: /清空队列
|
||||
cleartake:
|
||||
description: 清空DB队列中未回应的web同步任务(管理员)
|
||||
usage: /cleartake
|
||||
|
||||
Reference in New Issue
Block a user