第29批:title/tellraw报错接管恢复压制原生报错并直接打开JSON构造器(附标准示例+/json超链接) + 打包箱放置仅限领地主/授权成员且放下清除自定义名 + 封禁对账收回回填created创建时间(修复拷贝生产封禁数据后全员播报乱叫)
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m44s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s

This commit is contained in:
youpaishidifu
2026-10-17 13:37:16 +08:00
parent 2ee874eeda
commit 12e68060fa
4 changed files with 79 additions and 12 deletions
@@ -223,10 +223,45 @@ public class ChestCarryManager implements Listener {
if (data == null || data.isEmpty()) return;
Block placed = e.getBlockPlaced();
// ★ 29批第4项:打包箱仅领地主/授权成员/管理员可放置 ——
// 防止访客/路人拿到打包箱后就地放下自动回填、窃取他人财物。
// 无权限 → 取消放置(打包箱原封不动留在手上,绝不吞物);
// 放置点不在任何领地内 → 无人管辖,交还原版正常回填。
AreaProtection ap = main.areaProtection;
if (ap != null) {
AreaProtection.AreaConfig ac = ap.getArea(
placed.getWorld().getName(), placed.getX(), placed.getY(), placed.getZ());
if (ac != null) {
AreaProtection.PermissionLevel lv = ap.getPermissionLevel(e.getPlayer(), ac);
if (lv != AreaProtection.PermissionLevel.ADMIN
&& lv != AreaProtection.PermissionLevel.OWNER
&& lv != AreaProtection.PermissionLevel.MEMBER) {
e.setCancelled(true);
e.getPlayer().sendMessage("§c[箱子搬运] §7只有领地主/授权成员能在此放置打包箱");
return;
}
}
}
// 1 tick 后方块状态就绪再回填(含与邻半合并成大箱子的情况)
plugin.getServer().getScheduler().runTask(plugin, () -> {
BlockState st = placed.getState();
if (!(st instanceof org.bukkit.block.Container)) return;
// ★ 29批第2项:放下的是「原版普通箱子」——物品的自定义名可能被方块继承成
// CustomName(准星指向时显示「打包箱」),这里清掉并落盘,再回填内容。
// update(force=true, applyPhysics=false):强制写回世界、不触发邻块物理。
try {
// Paper 26:BlockState 已无 setCustomName —— 方块实体名在 Nameable 上
//(Container ← LockableTileState ← Nameable),setter 是 adventure
// 的 customName(Component),null 直传无歧义
((org.bukkit.Nameable) st).customName(null);
st.update(true, false);
} catch (Exception ex) {
// 个别容器不支持改名就忽略,回填照常进行
}
st = placed.getState(); // update 后旧快照失效,重新取
if (!(st instanceof org.bukkit.block.Container)) return;
try {
ItemStack[] items = InventorySerializer.fromBase64(data);
Inventory inv = ((org.bukkit.block.Container) st).getInventory();
+25 -7
View File
@@ -234,12 +234,12 @@ public class JsonMessageGui implements Listener {
/**
* 原版 title/tellraw 报错接管(挂在 Main.onUnknownCommand)。
*
* <p>★ 28批(用户要求):不再直接 openInventory 强接管——运维手打 JSON 出错时
* 瞬间被弹走面板很突兀。改为「保留原生最小化报错」(Main 侧不再 suppressUnknown),
* 本方法只做「预填原文 + 提示 + 可点击超链接」,由玩家自己点链接唤醒构造器。
* <p>★ 29批(用户要求,回退28批「不直接打开」的决定):Main 侧 suppressUnknown
* 压掉原生报错后由本方法接管 ——「预填原文 + 标准最小化写法示例 + /json CLI
* 可点击超链接 + 直接 openInventory 打开构造器」。
*
* @param body 已去掉开头斜杠的命令行
* @return true = 已预填并发出提示+超链接(原生报错保留),false = 不归本构造器管
* @return true = 已预填并接管(原生报错由 Main 压制),false = 不归本构造器管
*/
public boolean tryTakeover(Player p, String body) {
if (body.isEmpty()) return false;
@@ -281,24 +281,42 @@ public class JsonMessageGui implements Listener {
s.styleFlags = 0;
if (!extracted.isEmpty()) s.text.append(extracted);
// ★ 28批:延迟 1 tick 发提示——先让原生最小化报错落地,再看到提示+超链接
// ★ 29批:延迟 1 tick —— 先让 suppressUnknown 压掉的原生报错彻底落地,
// 再发「标准最小化示例 + /json CLI 连接」并直接打开构造器(回退28批不打开的决定)
plugin.getServer().getScheduler().runTask(plugin, () -> {
p.sendMessage(net.kyori.adventure.text.Component
.text("[JSON构造器] ", net.kyori.adventure.text.format.NamedTextColor.GOLD)
.append(net.kyori.adventure.text.Component.text(
"你的原文已预填 → ",
"原文已预填,构造器已打开。标准写法示例:",
net.kyori.adventure.text.format.NamedTextColor.GRAY))
.append(net.kyori.adventure.text.Component.text(
"[点击唤醒构造器]",
exampleCmdOf(mode),
net.kyori.adventure.text.format.NamedTextColor.WHITE))
.append(net.kyori.adventure.text.Component.newline())
.append(net.kyori.adventure.text.Component.text(
"CLI 连接(备用入口):",
net.kyori.adventure.text.format.NamedTextColor.GRAY))
.append(net.kyori.adventure.text.Component.text(
"[/json]",
net.kyori.adventure.text.format.NamedTextColor.YELLOW,
net.kyori.adventure.text.format.TextDecoration.UNDERLINED)
.clickEvent(net.kyori.adventure.text.event.ClickEvent.runCommand("/json"))
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.showText(
net.kyori.adventure.text.Component.text("打开 JSON 构造器(原文已预填)")))));
open(p);
});
return true;
}
/** 29批:各模式的标准最小化写法示例(压制原生报错后由本构造器给出) */
private static String exampleCmdOf(int mode) {
switch (mode) {
case MODE_TITLE: return "/title @a title {\"text\":\"标题\"}";
case MODE_SUBTITLE: return "/title @a subtitle {\"text\":\"副标题\"}";
default: return "/tellraw @a {\"text\":\"你好\"}";
}
}
/**
* 28批:从 tellraw 载荷里剥掉目标选择器——原版结构 tellraw <target> <json>,
* @a/@p/玩家名属于指令参数,不能被构造成用户文本(用户实测 @a 被塞进了 text)。
+5 -3
View File
@@ -4515,9 +4515,11 @@ public class Main extends JavaPlugin
if (lowerBody.startsWith("tellraw ")
|| lowerBody.startsWith("title ")) {
if (jsonGui.tryTakeover((Player) sender, body)) {
// 28批:不再 suppressUnknown——按用户要求保留原生最小化报错,
// tryTakeover 只负责预填+发提示超链接(由玩家点链接唤醒GUI)
getLogger().info("[无感切换] title/tellraw 报错已提示进JSON构造器: \""
// ★ 29批(回退28批):压制原生 "<--[HERE]" 报错 —— tryTakeover 接管后
// 自己发「标准最小化示例 + /json CLI超链接」并直接打开构造器,
// 原生那行报错按用户要求压掉
suppressUnknown(e);
getLogger().info("[无感切换] title/tellraw 报错已接管进JSON构造器: \""
+ body + "\"");
return;
}
+14 -2
View File
@@ -4294,11 +4294,15 @@ public class WebManager {
new String[]{},
alignMap(), false, false));
// —— 封禁名单(Web 后台加的封禁要拉回游戏内生效)——
// ★ 29批:map 增加 created_time→created —— PHP check_alignment 的 cols 已带上
// created_time,收回行才能携带真实封禁创建时间;hashcols 故意不含 created,
// 指纹不变(防两端 created 精度/缺失差异恒 changed 死循环)。
ALIGN_CFGS.put("bans", new AlignCfg("bans",
new String[]{"target", "ban_type"},
new String[]{"target", "ban_type", "reason", "source", "expire_time"},
new String[]{"expire_time"},
alignMap("ban_type", "type", "expire_time", "expire"), true, false));
alignMap("ban_type", "type", "expire_time", "expire",
"created_time", "created"), true, false));
// —— 领地数据(PHP web_area_lands;游戏内 area_lands 为唯一权威)——
// pull=false:Web 后台的领地改动不回流 Java(与改造前一致)。
// pushOnExtra=true:PHP 多出的行(游戏里已删 / 后台自建)→ 触发一次整包推,
@@ -4763,8 +4767,16 @@ public class WebManager {
}
// 真实签名:addBan(target, reason, expires, source)
// Date 必须全限定——本文件同时 import 了 java.sql.* 与 java.util.*
banList.addBan(target, reason,
// ★ 29批:收回行现在携带 PHP 侧创建时间(check_alignment cols 已含
// created_time→created)。addBan 默认 created=now,若不回填,
// 生产封禁数据拷到测试服后「收回」一轮会把所有封禁的创建时间刷成
// 当天 → 封禁播报全员乱叫(2026-10-22 用户实测)。
org.bukkit.BanEntry<?> pulled = banList.addBan(target, reason,
expire > 0 ? new java.util.Date(expire) : null, source);
long created = longOf(row.get("created"));
if (pulled != null && created > 0) {
pulled.setCreated(new java.util.Date(created));
}
plugin.getLogger().info("[对账] 已收回 Web 端封禁: " + (isIp ? "IP " : "") + target);
return true;
}