第二十六批:箱子搬运(精准采集斧拆箱2秒窗口打包+放置回填)、/json消息构造器GUI(tellraw/标题/字幕+颜色+原版报错接管)、背包备份改每小时0/15/30/45分准点
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockState;
|
||||
import org.bukkit.block.data.type.Chest;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.persistence.PersistentDataContainer;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 箱子搬运(26批,复刻 Chest Transport mod 玩法)
|
||||
*
|
||||
* 触发条件(全部满足才接管,否则一律交原版):
|
||||
* 1. 领地内(AreaProtection.getArea 命中)
|
||||
* 2. 权限级别为 ADMIN / OWNER / MEMBER(领地主/授权成员/管理员)
|
||||
* 3. 主手是斧头(*_AXE)且带精准采集魔咒
|
||||
*
|
||||
* 2 秒窗口语义(2026-10-16 用户确认):
|
||||
* 「连续破坏是2个箱子1组,中间断了,如果是大箱子,就打包被拆的那一半的内容」
|
||||
* - 首破挂起观察 2 秒(40 tick):不立即给打包箱,删原版掉落
|
||||
* - 2 秒内同玩家再拆一个箱子 → 成组,一并交付 2 个打包箱
|
||||
* - 超时(链断)→ 单独交付那 1 个;大箱子天然只快照被拆那一半
|
||||
* (Chest.getBlockInventory() 只含被拆半边的 27 格)
|
||||
*
|
||||
* 打包箱:Material.CHEST / TRAPPED_CHEST + 名称标识 + PDC 存 InventorySerializer
|
||||
* base64 内容;setMaxStackSize(1) 防不同内容堆叠混淆。
|
||||
* 放置回填:BlockPlaceEvent 后 1 tick 方块状态就绪再灌回 27 格。
|
||||
*/
|
||||
public class ChestCarryManager implements Listener {
|
||||
|
||||
private final Main main;
|
||||
private final Plugin plugin;
|
||||
private final NamespacedKeyRef carryKey;
|
||||
|
||||
/** 组队窗口 2 秒 = 40 tick */
|
||||
private static final long WINDOW_TICKS = 40L;
|
||||
private static final long WINDOW_MS = 2000L;
|
||||
|
||||
/** PDC key 的小包装(避免与 block data 的 Chest 类名混淆的命名负担) */
|
||||
private static final class NamespacedKeyRef {
|
||||
final org.bukkit.NamespacedKey key;
|
||||
NamespacedKeyRef(Plugin plugin, String name) {
|
||||
this.key = new org.bukkit.NamespacedKey(plugin, name);
|
||||
}
|
||||
}
|
||||
|
||||
/** 首破挂起中的搬运记录 */
|
||||
private static final class PendingCarry {
|
||||
final UUID player;
|
||||
final String worldName;
|
||||
final Location dropLoc; // 超时且玩家离线时的兜底投递点
|
||||
final boolean trapped; // 来源是否陷阱箱
|
||||
final String halfName; // 小箱子 / 大箱子·左半 / 大箱子·右半
|
||||
final ItemStack[] contents; // 被拆半边 27 格快照
|
||||
final long ts; // 挂起时刻(组队窗口判定)
|
||||
|
||||
PendingCarry(UUID player, String worldName, Location dropLoc,
|
||||
boolean trapped, String halfName, ItemStack[] contents, long ts) {
|
||||
this.player = player;
|
||||
this.worldName = worldName;
|
||||
this.dropLoc = dropLoc;
|
||||
this.trapped = trapped;
|
||||
this.halfName = halfName;
|
||||
this.contents = contents;
|
||||
this.ts = ts;
|
||||
}
|
||||
}
|
||||
|
||||
/** 玩家 UUID → 挂起中的搬运(每人最多 1 条) */
|
||||
private final Map<UUID, PendingCarry> pending = new ConcurrentHashMap<>();
|
||||
|
||||
public ChestCarryManager(Main main) {
|
||||
this.main = main;
|
||||
this.plugin = main;
|
||||
this.carryKey = new NamespacedKeyRef(main, "chest_carry_data");
|
||||
}
|
||||
|
||||
// ==================== 拆箱:拦截 → 挂起/成组 ====================
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onChestBreak(BlockBreakEvent e) {
|
||||
Material mat = e.getBlock().getType();
|
||||
if (mat != Material.CHEST && mat != Material.TRAPPED_CHEST) return;
|
||||
Player p = e.getPlayer();
|
||||
|
||||
// 手持:主手必须是精准采集斧头,否则交原版
|
||||
ItemStack hand = p.getInventory().getItemInMainHand();
|
||||
if (hand == null || hand.getType() == Material.AIR) return;
|
||||
if (!hand.getType().name().endsWith("_AXE")) return;
|
||||
if (!hand.containsEnchantment(Enchantment.SILK_TOUCH)) return;
|
||||
|
||||
// 领地 + 权限:非领地、或非 ADMIN/OWNER/MEMBER 一律交原版
|
||||
AreaProtection ap = main.areaProtection;
|
||||
if (ap == null) return;
|
||||
Block b = e.getBlock();
|
||||
AreaProtection.AreaConfig ac =
|
||||
ap.getArea(b.getWorld().getName(), b.getX(), b.getY(), b.getZ());
|
||||
if (ac == null) return;
|
||||
AreaProtection.PermissionLevel lv = ap.getPermissionLevel(p, ac);
|
||||
if (lv != AreaProtection.PermissionLevel.ADMIN
|
||||
&& lv != AreaProtection.PermissionLevel.OWNER
|
||||
&& lv != AreaProtection.PermissionLevel.MEMBER) return;
|
||||
|
||||
// 快照被拆半边内容(大箱子 getBlockInventory 只含这半边 27 格)
|
||||
BlockState st = b.getState();
|
||||
if (!(st instanceof org.bukkit.block.Chest)) return;
|
||||
org.bukkit.block.Chest chest = (org.bukkit.block.Chest) st;
|
||||
ItemStack[] snap = chest.getBlockInventory().getContents();
|
||||
|
||||
// 全空箱不接管:交原版掉箱子本体,无物品洪水
|
||||
if (isEmpty(snap)) return;
|
||||
|
||||
// 大/小箱子归属名(TRAPPED_CHEST 的 block data 也是 Chest 类)
|
||||
String halfName = "小箱子";
|
||||
org.bukkit.block.data.BlockData bd = b.getBlockData();
|
||||
if (bd instanceof Chest) {
|
||||
Chest.Type t = ((Chest) bd).getType();
|
||||
if (t == Chest.Type.LEFT) halfName = "大箱子·左半";
|
||||
else if (t == Chest.Type.RIGHT) halfName = "大箱子·右半";
|
||||
}
|
||||
boolean trapped = (mat == Material.TRAPPED_CHEST);
|
||||
|
||||
// 接管:删原版大量掉落
|
||||
e.setDropItems(false);
|
||||
|
||||
UUID id = p.getUniqueId();
|
||||
PendingCarry prev = pending.remove(id);
|
||||
long now = System.currentTimeMillis();
|
||||
if (prev != null && (now - prev.ts) <= WINDOW_MS) {
|
||||
// 成组:2 箱 1 组,一并交付
|
||||
deliver(p, prev);
|
||||
deliver(p, new PendingCarry(id, b.getWorld().getName(),
|
||||
p.getLocation(), trapped, halfName, snap, now));
|
||||
} else {
|
||||
// 首破:挂起观察 2 秒,等第二破成组或超时单独交付
|
||||
PendingCarry cur = new PendingCarry(id, b.getWorld().getName(),
|
||||
p.getLocation(), trapped, halfName, snap, now);
|
||||
pending.put(id, cur);
|
||||
plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
|
||||
PendingCarry pc = pending.get(id);
|
||||
if (pc != cur) return; // 已成组交付或被新记录替换
|
||||
pending.remove(id);
|
||||
Player online = Bukkit.getPlayer(id);
|
||||
if (online != null && online.isOnline()) {
|
||||
deliver(online, pc);
|
||||
} else {
|
||||
// 玩家离线兜底:打包箱掉在原地
|
||||
try {
|
||||
World w = Bukkit.getWorld(pc.worldName);
|
||||
if (w != null) {
|
||||
w.dropItemNaturally(pc.dropLoc,
|
||||
buildPackedChest(pc.trapped, pc.halfName, pc.contents));
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
plugin.getLogger().warning(
|
||||
"[箱子搬运] 离线兜底掉落失败: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
}, WINDOW_TICKS);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 放置:回填内容 ====================
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
|
||||
public void onChestPlace(BlockPlaceEvent e) {
|
||||
ItemStack it = e.getItemInHand();
|
||||
if (it == null) return;
|
||||
if (it.getType() != Material.CHEST && it.getType() != Material.TRAPPED_CHEST) return;
|
||||
if (!it.hasItemMeta()) return;
|
||||
PersistentDataContainer pdc = it.getItemMeta().getPersistentDataContainer();
|
||||
if (!pdc.has(carryKey.key, PersistentDataType.STRING)) return;
|
||||
String data = pdc.get(carryKey.key, PersistentDataType.STRING);
|
||||
if (data == null || data.isEmpty()) return;
|
||||
|
||||
Block placed = e.getBlockPlaced();
|
||||
// 1 tick 后方块状态就绪再回填(含与邻半合并成大箱子的情况)
|
||||
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||
BlockState st = placed.getState();
|
||||
if (!(st instanceof org.bukkit.block.Chest)) return;
|
||||
try {
|
||||
ItemStack[] items = InventorySerializer.fromBase64(data);
|
||||
org.bukkit.block.Chest chest = (org.bukkit.block.Chest) st;
|
||||
Inventory inv = chest.getBlockInventory();
|
||||
inv.clear();
|
||||
int n = Math.min(items.length, 27);
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (items[i] != null) inv.setItem(i, items[i]);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
plugin.getLogger().warning("[箱子搬运] 放置回填失败: " + ex.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 内部工具 ====================
|
||||
|
||||
/** 交付一个打包箱给玩家(背包满则掉脚下),并提示 */
|
||||
private void deliver(Player p, PendingCarry c) {
|
||||
try {
|
||||
ItemStack packed = buildPackedChest(c.trapped, c.halfName, c.contents);
|
||||
HashMap<Integer, ItemStack> left = p.getInventory().addItem(packed);
|
||||
if (!left.isEmpty()) {
|
||||
left.values().forEach(it ->
|
||||
p.getWorld().dropItemNaturally(p.getLocation(), it));
|
||||
}
|
||||
p.sendMessage("§a[箱子搬运] §7已打包 §f" + c.halfName
|
||||
+ "§7(" + countItems(c.contents) + " 件物品),放下自动回填");
|
||||
} catch (Exception ex) {
|
||||
// 序列化失败兜底:内容直接掉落,绝不吞物
|
||||
p.sendMessage("§c[箱子搬运] §7打包失败,物品已直接掉落");
|
||||
for (ItemStack it : c.contents) {
|
||||
if (it != null && !it.getType().isAir()) {
|
||||
p.getWorld().dropItemNaturally(p.getLocation(), it);
|
||||
}
|
||||
}
|
||||
plugin.getLogger().warning("[箱子搬运] 打包失败: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 构造打包箱物品:名称标识 + 防堆叠 + PDC 内容 */
|
||||
private ItemStack buildPackedChest(boolean trapped, String halfName,
|
||||
ItemStack[] contents) throws Exception {
|
||||
ItemStack it = new ItemStack(trapped ? Material.TRAPPED_CHEST : Material.CHEST);
|
||||
ItemMeta meta = it.getItemMeta();
|
||||
meta.setDisplayName("§b§l打包箱 §7(" + halfName + ")");
|
||||
List<String> lore = new ArrayList<>();
|
||||
lore.add("§7内含 §f" + countItems(contents) + " §7件物品");
|
||||
lore.add("§a放置后 §7自动回填内容");
|
||||
meta.setLore(lore);
|
||||
// 不同内容绝不堆叠,否则 PDC 串味
|
||||
meta.setMaxStackSize(1);
|
||||
meta.getPersistentDataContainer().set(carryKey.key,
|
||||
PersistentDataType.STRING, InventorySerializer.toBase64(contents));
|
||||
it.setItemMeta(meta);
|
||||
return it;
|
||||
}
|
||||
|
||||
private static boolean isEmpty(ItemStack[] a) {
|
||||
if (a == null || a.length == 0) return true;
|
||||
for (ItemStack i : a) {
|
||||
if (i != null && !i.getType().isAir()) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int countItems(ItemStack[] a) {
|
||||
int n = 0;
|
||||
for (ItemStack i : a) {
|
||||
if (i != null && !i.getType().isAir()) n += i.getAmount();
|
||||
}
|
||||
return n;
|
||||
}
|
||||
}
|
||||
@@ -401,7 +401,7 @@ public class DatabaseManager {
|
||||
+ "time INTEGER"
|
||||
+ ")");
|
||||
|
||||
// 背包自动滚动备份表(15分钟定时):每位玩家保留最近 3 份
|
||||
// 背包自动滚动备份表(每小时 0/15/30/45 分准点定时):每位玩家保留最近 3 份
|
||||
st.execute("CREATE TABLE IF NOT EXISTS "
|
||||
+ "inv_backup_auto ("
|
||||
+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
@@ -3014,7 +3014,7 @@ public class DatabaseManager {
|
||||
return deleted;
|
||||
}
|
||||
|
||||
// ==================== 背包自动滚动备份(15分钟定时)====================
|
||||
// ==================== 背包自动滚动备份(每小时 0/15/30/45 分准点定时)====================
|
||||
|
||||
/**
|
||||
* 保存背包自动备份 —— 追加写入,每位玩家仅保留最近 3 份。
|
||||
|
||||
@@ -10,7 +10,7 @@ import java.io.IOException;
|
||||
* 背包滚动自动备份。
|
||||
* <p>
|
||||
* ★ 功能:
|
||||
* 1. 由 Main 的 15 分钟定时任务驱动,遍历所有在线玩家。
|
||||
* 1. 由 Main 的准点定时任务驱动(第26批:每小时 0/15/30/45 分墙钟对齐,不再按启动时刻漂移),遍历所有在线玩家。
|
||||
* 2. 在 PVP 竞技场 / 练习场(测试场)的玩家不备份 —— 避免竞技场装备覆盖真实背包。
|
||||
* 3. 序列化 主背包 / 护甲 / 副手(extra) / 等级 / 经验,写入 inv_backup_auto 表。
|
||||
* 4. 每玩家保留最近 3 份(滚动覆盖),由 DatabaseManager.saveAutoBackup 负责清理。
|
||||
@@ -22,7 +22,7 @@ public class InventoryAutoBackup {
|
||||
|
||||
private final Main plugin;
|
||||
|
||||
/** 备份周期:15 分钟(单位 tick) */
|
||||
/** 备份周期:15 分钟(单位 tick)。★ 第26批起 Main 改墙钟对齐调度(:00/:15/:30/:45 分),此常量仅作周期语义参考,不再直接作调度入参。 */
|
||||
public static final long INTERVAL_TICKS = 15L * 60L * 20L;
|
||||
|
||||
public InventoryAutoBackup(Main plugin) {
|
||||
|
||||
@@ -0,0 +1,700 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryDragEvent;
|
||||
import org.bukkit.event.player.AsyncPlayerChatEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* /json 消息构造器 GUI(26批 任务170)。
|
||||
*
|
||||
* <p><b>功能</b>:
|
||||
* <ul>
|
||||
* <li>27 格小箱子面板:3 种模式(tellraw 原生 JSON 消息 / 大标题 / 小标题)</li>
|
||||
* <li>颜色行 = 自定义颜色输入 + 调色板(解码配置.md 自定义色优先,内置常用色补足)</li>
|
||||
* <li>文字经聊天栏捕获(GUI 内开不了聊天,先关面板再输入)</li>
|
||||
* <li>发送 = 以玩家自己身份 dispatchCommand(tellraw/title 天然受原版权限约束,
|
||||
* OP/管理员可全服广播,普通玩家与直接敲原版指令行为一致)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>触发</b>:/json 指令;或原版 title/tellraw 报错时由 Main.onUnknownCommand
|
||||
* 调 {@link #tryTakeover} 接管并提取用户原文预填。
|
||||
*
|
||||
* <p><b>颜色合法性</b>(仅枚举 MC 原生支持,Wiki 所列):
|
||||
* 16 色英文名 / #RRGGBB 十六进制 / R G B 数值;其余一律「颜色配置无效」。
|
||||
*
|
||||
* <p><b>解码配置.md</b>:启动自动释放到插件目录,表格 split 取 cells[length-1],
|
||||
* 「配置区域开始/结束」标记之间逐行解析;无效色收集后提示。
|
||||
*/
|
||||
public class JsonMessageGui implements Listener {
|
||||
|
||||
private final Main plugin;
|
||||
static final String TITLE = "§6§lJSON消息构造器";
|
||||
|
||||
// ==================== 模式 ====================
|
||||
private static final int MODE_TELLRAW = 0;
|
||||
private static final int MODE_TITLE = 1;
|
||||
private static final int MODE_SUBTITLE = 2;
|
||||
|
||||
// ==================== MC 原生 16 色(Wiki 所列) ====================
|
||||
private static final Set<String> NATIVE_COLORS = new LinkedHashSet<>(Arrays.asList(
|
||||
"black", "dark_blue", "dark_green", "dark_aqua",
|
||||
"dark_red", "dark_purple", "gold", "gray",
|
||||
"dark_gray", "blue", "green", "aqua",
|
||||
"red", "light_purple", "yellow", "white"));
|
||||
|
||||
/** 内置常用色(调色板补位用) */
|
||||
private static final List<String> BUILTIN_COMMON = Arrays.asList(
|
||||
"red", "gold", "yellow", "green",
|
||||
"aqua", "blue", "light_purple", "white");
|
||||
|
||||
// ==================== 格位布局 ====================
|
||||
private static final int SLOT_MODE_TELLRAW = 1;
|
||||
private static final int SLOT_MODE_TITLE = 3;
|
||||
private static final int SLOT_MODE_SUBTITLE = 5;
|
||||
private static final int SLOT_HELP = 7;
|
||||
private static final int SLOT_CUSTOM_COLOR = 9;
|
||||
private static final int SLOT_PALETTE_FIRST = 10; // 10-17 共 8 格调色板
|
||||
private static final int SLOT_PALETTE_LAST = 17;
|
||||
private static final int SLOT_PREVIEW = 21;
|
||||
private static final int SLOT_INPUT_TEXT = 22;
|
||||
private static final int SLOT_CLEAR = 25;
|
||||
private static final int SLOT_SEND = 26;
|
||||
|
||||
// ==================== 每玩家会话 ====================
|
||||
private static final class Session {
|
||||
int mode = MODE_TELLRAW;
|
||||
/** 已确认的文本段(含 § 颜色码) */
|
||||
final StringBuilder text = new StringBuilder();
|
||||
/** 当前等待聊天捕获的输入类型:null=无 */
|
||||
String capture; // "text" | "customcolor"
|
||||
Session(int mode) { this.mode = mode; }
|
||||
}
|
||||
|
||||
private final Map<UUID, Session> sessions = new ConcurrentHashMap<>();
|
||||
|
||||
/** 调色板(md 自定义色优先,内置常用色补足到 8 格) */
|
||||
private final List<String> palette = new ArrayList<>();
|
||||
/** md 中解析出的无效色(提示用) */
|
||||
private final List<String> invalidColors = new ArrayList<>();
|
||||
|
||||
public JsonMessageGui(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
loadPalette();
|
||||
}
|
||||
|
||||
// ==================== 解码配置.md 解析 ====================
|
||||
|
||||
/** 重新加载调色板:md 自定义色(配置区域标记间表格 cells[length-1])+ 内置补足 */
|
||||
private void loadPalette() {
|
||||
palette.clear();
|
||||
invalidColors.clear();
|
||||
File f = new File(plugin.getDataFolder(), "json解码配置/解码配置.md");
|
||||
if (f.exists()) {
|
||||
boolean inRegion = false;
|
||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(
|
||||
new FileInputStream(f), StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
String t = line.trim();
|
||||
if (t.startsWith("##")) {
|
||||
// 标题行携带区域开关
|
||||
inRegion = t.contains("配置区域开始");
|
||||
if (t.contains("配置区域结束")) inRegion = false;
|
||||
continue;
|
||||
}
|
||||
if (!inRegion) continue;
|
||||
if (!t.startsWith("|")) continue;
|
||||
String[] cells = t.split("\\|");
|
||||
if (cells.length < 2) continue;
|
||||
String raw = cells[cells.length - 1].trim();
|
||||
// 表头/分隔行跳过
|
||||
if (raw.isEmpty() || raw.equals("---")
|
||||
|| raw.equals("颜色代码")) continue;
|
||||
if (isValidColor(raw)) {
|
||||
String norm = normalizeColor(raw);
|
||||
if (!palette.contains(norm)) palette.add(norm);
|
||||
} else {
|
||||
invalidColors.add(raw);
|
||||
plugin.getLogger().warning(
|
||||
"[JSON构造器] 颜色配置无效: " + raw);
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
plugin.getLogger().warning(
|
||||
"[JSON构造器] 解码配置.md 读取失败: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
// 内置常用色补足到 8 格
|
||||
for (String c : BUILTIN_COMMON) {
|
||||
if (palette.size() >= SLOT_PALETTE_LAST - SLOT_PALETTE_FIRST + 1) break;
|
||||
if (!palette.contains(c)) palette.add(c);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 颜色校验(仅 MC 原生支持) ====================
|
||||
|
||||
/** 16色英文名 / #RRGGBB / R G B 数值 → 合法;其余 false */
|
||||
static boolean isValidColor(String s) {
|
||||
if (s == null) return false;
|
||||
String t = s.trim().toLowerCase(Locale.ROOT);
|
||||
if (t.isEmpty()) return false;
|
||||
if (NATIVE_COLORS.contains(t)) return true;
|
||||
// 十六进制 #RRGGBB(也接受不带#的6位hex)
|
||||
if (t.matches("#?[0-9a-f]{6}")) return true;
|
||||
// R G B:R255 G128 B0(大小写不敏感,逗号/空格分隔均可)
|
||||
if (t.matches("(?i)r\\s*\\d{1,3}\\s*[,;,;]?\\s*g\\s*\\d{1,3}\\s*[,;,;]?\\s*b\\s*\\d{1,3}")) {
|
||||
Matcher m = Pattern.compile("(?i)(\\d{1,3})[^0-9]*(\\d{1,3})[^0-9]*(\\d{1,3})")
|
||||
.matcher(t);
|
||||
if (m.find()) {
|
||||
return inByte(m.group(1)) && inByte(m.group(2)) && inByte(m.group(3));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean inByte(String n) {
|
||||
int v = Integer.parseInt(n);
|
||||
return v >= 0 && v <= 255;
|
||||
}
|
||||
|
||||
/** 合法色归一化:hex 统一带#小写;RGB 转 #RRGGBB;英文名小写 */
|
||||
static String normalizeColor(String s) {
|
||||
String t = s.trim().toLowerCase(Locale.ROOT);
|
||||
if (NATIVE_COLORS.contains(t)) return t;
|
||||
if (t.matches("#?[0-9a-f]{6}")) {
|
||||
return t.startsWith("#") ? t : ("#" + t);
|
||||
}
|
||||
Matcher m = Pattern.compile("(?i)(\\d{1,3})[^0-9]*(\\d{1,3})[^0-9]*(\\d{1,3})")
|
||||
.matcher(t);
|
||||
if (m.find()) {
|
||||
return String.format("#%02x%02x%02x",
|
||||
Integer.parseInt(m.group(1)),
|
||||
Integer.parseInt(m.group(2)),
|
||||
Integer.parseInt(m.group(3)));
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
// ==================== 入口 ====================
|
||||
|
||||
/** /json 指令入口 */
|
||||
public void open(Player p) {
|
||||
Session s = sessions.computeIfAbsent(
|
||||
p.getUniqueId(), k -> new Session(MODE_TELLRAW));
|
||||
s.capture = null;
|
||||
if (!invalidColors.isEmpty()) {
|
||||
p.sendMessage("§e[JSON构造器] §7解码配置.md 中 §c"
|
||||
+ invalidColors.size() + " §7个颜色配置无效(仅支持 MC 原生色名/十六进制/RGB),已忽略");
|
||||
}
|
||||
p.openInventory(build(s));
|
||||
}
|
||||
|
||||
/**
|
||||
* 原版 title/tellraw 报错接管(挂在 Main.onUnknownCommand)。
|
||||
*
|
||||
* @param body 已去掉开头斜杠的命令行
|
||||
* @return true = 已接管(打开GUI预填原文),false = 不归本构造器管
|
||||
*/
|
||||
public boolean tryTakeover(Player p, String body) {
|
||||
if (body.isEmpty()) return false;
|
||||
String lower = body.toLowerCase(Locale.ROOT);
|
||||
int mode;
|
||||
String payload;
|
||||
if (lower.startsWith("tellraw ")) {
|
||||
mode = MODE_TELLRAW;
|
||||
payload = body.substring(8).trim();
|
||||
} else if (lower.startsWith("title ")) {
|
||||
// title <target> title|subtitle|actionbar <json...>
|
||||
String rest = body.substring(6).trim();
|
||||
int sp1 = rest.indexOf(' ');
|
||||
if (sp1 < 0) return false;
|
||||
String afterTarget = rest.substring(sp1 + 1).trim();
|
||||
int sp2 = afterTarget.indexOf(' ');
|
||||
String kind = (sp2 < 0 ? afterTarget : afterTarget.substring(0, sp2))
|
||||
.toLowerCase(Locale.ROOT);
|
||||
if (kind.equals("title")) {
|
||||
mode = MODE_TITLE;
|
||||
} else if (kind.equals("subtitle")) {
|
||||
mode = MODE_SUBTITLE;
|
||||
} else {
|
||||
return false; // actionbar 不在3模式内
|
||||
}
|
||||
payload = sp2 < 0 ? "" : afterTarget.substring(sp2 + 1).trim();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 提取用户原文(JSON 里 text 字段,或原样串)
|
||||
String extracted = extractUserText(payload);
|
||||
Session s = sessions.computeIfAbsent(p.getUniqueId(), k -> new Session(mode));
|
||||
s.mode = mode;
|
||||
s.capture = null;
|
||||
s.text.setLength(0);
|
||||
if (!extracted.isEmpty()) s.text.append(extracted);
|
||||
|
||||
p.openInventory(build(s));
|
||||
p.sendMessage("§e[JSON构造器] §7已接管原版报错,你的原文已填入,"
|
||||
+ "选颜色/改内容后点§a发送§7即可(也可继续§f/json§7打开本面板)");
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 从 tellraw/title 的 JSON 载荷里提取用户原文 text;解析不了就原样返回 */
|
||||
static String extractUserText(String payload) {
|
||||
if (payload == null || payload.isEmpty()) return "";
|
||||
// 最常见 {"text":"..."} 或 {"text":"...","extra":[...]}
|
||||
Matcher m = Pattern.compile("\"text\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"")
|
||||
.matcher(payload);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while (m.find()) {
|
||||
String seg = m.group(1)
|
||||
.replace("\\\"", "\"")
|
||||
.replace("\\\\", "\\")
|
||||
.replace("\\n", "\n");
|
||||
sb.append(seg);
|
||||
}
|
||||
if (sb.length() > 0) return sb.toString();
|
||||
// 不是JSON(比如玩家直接打了中文)→ 原样
|
||||
if (!payload.startsWith("{")) return payload;
|
||||
return "";
|
||||
}
|
||||
|
||||
// ==================== GUI 构建 ====================
|
||||
|
||||
private Inventory build(Session s) {
|
||||
Inventory inv = Bukkit.createInventory(null, 27, TITLE);
|
||||
|
||||
inv.setItem(SLOT_MODE_TELLRAW, modeItem(
|
||||
Material.PAPER, "tellraw 原生JSON消息",
|
||||
s.mode == MODE_TELLRAW,
|
||||
"全服广播一条 JSON 消息", "对应 /tellraw @a ..."));
|
||||
inv.setItem(SLOT_MODE_TITLE, modeItem(
|
||||
Material.GOLD_BLOCK, "大标题",
|
||||
s.mode == MODE_TITLE,
|
||||
"全服屏幕中央大字", "对应 /title @a title ..."));
|
||||
inv.setItem(SLOT_MODE_SUBTITLE, modeItem(
|
||||
Material.NAME_TAG, "小标题",
|
||||
s.mode == MODE_SUBTITLE,
|
||||
"全服屏幕中央小字(大字下方)", "对应 /title @a subtitle ..."));
|
||||
|
||||
inv.setItem(SLOT_HELP, simple(Material.BOOK,
|
||||
"§f使用说明",
|
||||
"§71. 选模式 → 2. 点「输入内容」",
|
||||
"§7在聊天栏打字(自动收回面板)",
|
||||
"§3点击颜色按钮§7可给当前内容上色",
|
||||
"§7发送以你自己的身份执行原版指令,",
|
||||
"§7权限与直接敲 /tellraw、/title 完全一致"));
|
||||
|
||||
inv.setItem(SLOT_CUSTOM_COLOR, simple(Material.BLAZE_POWDER,
|
||||
"§b自定义颜色",
|
||||
"§7点击后在聊天栏输入颜色:",
|
||||
"§f#RRGGBB §7/ §fR255 G128 B0 §7/ §fred",
|
||||
"§c仅支持 MC 原生色,否则提示「颜色配置无效」"));
|
||||
|
||||
// 调色板 10-17
|
||||
for (int i = 0; i < (SLOT_PALETTE_LAST - SLOT_PALETTE_FIRST + 1); i++) {
|
||||
int slot = SLOT_PALETTE_FIRST + i;
|
||||
if (i < palette.size()) {
|
||||
String c = palette.get(i);
|
||||
inv.setItem(slot, simple(colorIcon(c),
|
||||
colorName(c) + "§7:点击上色",
|
||||
"§7代码 §f" + c));
|
||||
} else {
|
||||
inv.setItem(slot, simple(Material.LIGHT_GRAY_STAINED_GLASS_PANE,
|
||||
"§8空位", "§8(解码配置.md 可加自定义色)"));
|
||||
}
|
||||
}
|
||||
|
||||
inv.setItem(SLOT_PREVIEW, previewItem(s));
|
||||
inv.setItem(SLOT_INPUT_TEXT, simple(Material.WRITABLE_BOOK,
|
||||
"§a输入内容",
|
||||
"§7点击后在聊天栏输入文字",
|
||||
"§7(不会真的发到公屏)"));
|
||||
inv.setItem(SLOT_CLEAR, simple(Material.LAVA_BUCKET,
|
||||
"§c清空内容", "§7清空已输入的文本"));
|
||||
inv.setItem(SLOT_SEND, simple(Material.EMERALD_BLOCK,
|
||||
"§a§l发送", "§7以你的身份执行对应原版指令"));
|
||||
return inv;
|
||||
}
|
||||
|
||||
private ItemStack previewItem(Session s) {
|
||||
String shown = s.text.length() == 0 ? "§8(空)" : clip(s.text.toString(), 40);
|
||||
return simple(Material.HONEYCOMB, "§e当前内容预览", "§r" + shown);
|
||||
}
|
||||
|
||||
private static String clip(String s, int max) {
|
||||
String flat = s.replace('§', '&');
|
||||
return flat.length() <= max ? flat : flat.substring(0, max) + "…";
|
||||
}
|
||||
|
||||
private static String colorName(String code) {
|
||||
if (code.startsWith("#")) return "§" + hexToClosestLegacy(code) + "自定义" + code;
|
||||
// 16色名 → 对应 legacy 色显示
|
||||
String legacy = nativeToLegacy(code);
|
||||
return "§" + legacy + code;
|
||||
}
|
||||
|
||||
private static String nativeToLegacy(String name) {
|
||||
switch (name) {
|
||||
case "black": return "0";
|
||||
case "dark_blue": return "1";
|
||||
case "dark_green": return "2";
|
||||
case "dark_aqua": return "3";
|
||||
case "dark_red": return "4";
|
||||
case "dark_purple": return "5";
|
||||
case "gold": return "6";
|
||||
case "gray": return "7";
|
||||
case "dark_gray": return "8";
|
||||
case "blue": return "9";
|
||||
case "green": return "a";
|
||||
case "aqua": return "b";
|
||||
case "red": return "c";
|
||||
case "light_purple": return "d";
|
||||
case "yellow": return "e";
|
||||
default: return "f";
|
||||
}
|
||||
}
|
||||
|
||||
/** hex → 最接近的 legacy 色码(图标显示用) */
|
||||
private static char hexToClosestLegacy(String hex) {
|
||||
String h = hex.substring(1);
|
||||
int r = Integer.parseInt(h.substring(0, 2), 16);
|
||||
int g = Integer.parseInt(h.substring(2, 4), 16);
|
||||
int b = Integer.parseInt(h.substring(4, 6), 16);
|
||||
if (r < 64 && g < 64 && b < 64) return '8';
|
||||
if (r > 192 && g > 192 && b > 192) return 'f';
|
||||
if (r > g && r > b) return g > 128 ? '6' : 'c';
|
||||
if (g > r && g > b) return 'a';
|
||||
if (b > r && b > g) return '9';
|
||||
return '7';
|
||||
}
|
||||
|
||||
/** 调色板色对应的图标材料(16色用染料;hex用玻璃板) */
|
||||
private static Material colorIcon(String code) {
|
||||
switch (code) {
|
||||
case "black": return Material.BLACK_DYE;
|
||||
case "dark_blue": return Material.BLUE_DYE;
|
||||
case "dark_green": return Material.GREEN_DYE;
|
||||
case "dark_aqua": return Material.CYAN_DYE;
|
||||
case "dark_red": return Material.RED_DYE;
|
||||
case "dark_purple": return Material.PURPLE_DYE;
|
||||
case "gold": return Material.ORANGE_DYE;
|
||||
case "gray": return Material.GRAY_DYE;
|
||||
case "dark_gray": return Material.LIGHT_GRAY_DYE;
|
||||
case "blue": return Material.LIGHT_BLUE_DYE;
|
||||
case "green": return Material.LIME_DYE;
|
||||
case "aqua": return Material.LIGHT_BLUE_DYE;
|
||||
case "red": return Material.RED_DYE;
|
||||
case "light_purple": return Material.MAGENTA_DYE;
|
||||
case "yellow": return Material.YELLOW_DYE;
|
||||
case "white": return Material.WHITE_DYE;
|
||||
default: return Material.WHITE_STAINED_GLASS_PANE;
|
||||
}
|
||||
}
|
||||
|
||||
private static ItemStack modeItem(Material mat, String name, boolean selected,
|
||||
String... lore) {
|
||||
ItemStack it = simple(mat, (selected ? "§a✔ " : "§7") + name, lore);
|
||||
ItemMeta meta = it.getItemMeta();
|
||||
List<String> l = new ArrayList<>(meta.getLore() == null
|
||||
? java.util.Collections.emptyList() : meta.getLore());
|
||||
l.add(selected ? "§a(当前模式)" : "§e点击切换");
|
||||
meta.setLore(l);
|
||||
it.setItemMeta(meta);
|
||||
return it;
|
||||
}
|
||||
|
||||
private static ItemStack simple(Material mat, String name, String... lore) {
|
||||
ItemStack it = new ItemStack(mat);
|
||||
ItemMeta meta = it.getItemMeta();
|
||||
meta.setDisplayName(name);
|
||||
if (lore != null && lore.length > 0) {
|
||||
meta.setLore(Arrays.asList(lore));
|
||||
}
|
||||
it.setItemMeta(meta);
|
||||
return it;
|
||||
}
|
||||
|
||||
// ==================== 点击处理 ====================
|
||||
|
||||
@EventHandler
|
||||
public void onClick(InventoryClickEvent e) {
|
||||
if (!TITLE.equals(e.getView().getTitle())) return;
|
||||
e.setCancelled(true);
|
||||
if (!(e.getWhoClicked() instanceof Player)) return;
|
||||
Player p = (Player) e.getWhoClicked();
|
||||
Session s = sessions.get(p.getUniqueId());
|
||||
if (s == null) return;
|
||||
int slot = e.getRawSlot();
|
||||
if (slot < 0 || slot >= 27) return;
|
||||
|
||||
switch (slot) {
|
||||
case SLOT_MODE_TELLRAW:
|
||||
s.mode = MODE_TELLRAW; refresh(p, s); break;
|
||||
case SLOT_MODE_TITLE:
|
||||
s.mode = MODE_TITLE; refresh(p, s); break;
|
||||
case SLOT_MODE_SUBTITLE:
|
||||
s.mode = MODE_SUBTITLE; refresh(p, s); break;
|
||||
case SLOT_CUSTOM_COLOR:
|
||||
beginCapture(p, s, "customcolor"); break;
|
||||
case SLOT_INPUT_TEXT:
|
||||
beginCapture(p, s, "text"); break;
|
||||
case SLOT_CLEAR:
|
||||
s.text.setLength(0);
|
||||
p.sendMessage("§e[JSON构造器] §7内容已清空");
|
||||
refresh(p, s); break;
|
||||
case SLOT_SEND:
|
||||
doSend(p, s); break;
|
||||
default:
|
||||
if (slot >= SLOT_PALETTE_FIRST && slot <= SLOT_PALETTE_LAST) {
|
||||
int idx = slot - SLOT_PALETTE_FIRST;
|
||||
if (idx < palette.size()) {
|
||||
appendColor(p, s, palette.get(idx));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/** 拖拽进GUI一律无效 */
|
||||
@EventHandler
|
||||
public void onDrag(InventoryDragEvent e) {
|
||||
if (TITLE.equals(e.getView().getTitle())) e.setCancelled(true);
|
||||
}
|
||||
|
||||
private void refresh(Player p, Session s) {
|
||||
Inventory inv = build(s);
|
||||
// 原地重建:先关再开会闪,直接替换内容更稳
|
||||
Inventory cur = p.getOpenInventory().getTopInventory();
|
||||
if (cur != null && cur.getSize() == 27
|
||||
&& TITLE.equals(p.getOpenInventory().getTitle())) {
|
||||
cur.setContents(inv.getContents());
|
||||
} else {
|
||||
p.openInventory(inv);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 聊天捕获 ====================
|
||||
|
||||
private void beginCapture(Player p, Session s, String type) {
|
||||
s.capture = type;
|
||||
p.closeInventory();
|
||||
if (type.equals("text")) {
|
||||
p.sendMessage("§e[JSON构造器] §7请在聊天栏输入内容(§c不会发到公屏§7),"
|
||||
+ "§f空消息§7或§f/qj取消§7可放弃");
|
||||
} else {
|
||||
p.sendMessage("§e[JSON构造器] §7请在聊天栏输入颜色,如 §f#ff0000§7、"
|
||||
+ "§fR255 G0 B0§7、§fred§7;§f空消息§7可放弃");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 聊天捕获。★ AsyncPlayerChatEvent 在异步线程,
|
||||
* 严禁在此直接开库存/改背包 —— 一律 runTask 切主线程。
|
||||
*/
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onCaptureChat(AsyncPlayerChatEvent e) {
|
||||
Player p = e.getPlayer();
|
||||
Session s = sessions.get(p.getUniqueId());
|
||||
if (s == null || s.capture == null) return;
|
||||
// 捕获期间该玩家的话一律不上公屏
|
||||
e.setCancelled(true);
|
||||
String type = s.capture;
|
||||
String msg = e.getMessage();
|
||||
s.capture = null;
|
||||
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||
if (!p.isOnline()) return;
|
||||
if (msg.isEmpty() || msg.equals("/qj")) {
|
||||
p.sendMessage("§e[JSON构造器] §7已取消输入");
|
||||
} else if (type.equals("text")) {
|
||||
s.text.append(msg);
|
||||
p.sendMessage("§a[JSON构造器] §7已加入内容:§f" + clip(msg, 60));
|
||||
} else {
|
||||
// 自定义颜色
|
||||
if (isValidColor(msg)) {
|
||||
appendColor(p, s, normalizeColor(msg));
|
||||
} else {
|
||||
p.sendMessage("§c[JSON构造器] 颜色配置无效:§f" + msg
|
||||
+ "§7(仅支持 MC 原生16色名 / #RRGGBB / R G B)");
|
||||
}
|
||||
}
|
||||
p.openInventory(build(s));
|
||||
});
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onQuit(PlayerQuitEvent e) {
|
||||
sessions.remove(e.getPlayer().getUniqueId());
|
||||
}
|
||||
|
||||
/** 颜色写入当前文本(legacy § 码,hex 用 §x§R§R§G§G§B§B) */
|
||||
private void appendColor(Player p, Session s, String color) {
|
||||
if (color.startsWith("#")) {
|
||||
s.text.append('§').append('x');
|
||||
for (char c : color.substring(1).toCharArray()) {
|
||||
s.text.append('§').append(Character.toLowerCase(c));
|
||||
}
|
||||
} else {
|
||||
s.text.append('§').append(nativeToLegacy(color));
|
||||
}
|
||||
p.sendMessage("§a[JSON构造器] §7已上色 §f" + color);
|
||||
refresh(p, s);
|
||||
}
|
||||
|
||||
// ==================== 发送 ====================
|
||||
|
||||
private void doSend(Player p, Session s) {
|
||||
if (s.text.length() == 0) {
|
||||
p.sendMessage("§c[JSON构造器] 内容为空,先点「输入内容」");
|
||||
return;
|
||||
}
|
||||
String json = toTellrawJson(s.text.toString());
|
||||
String cmd;
|
||||
switch (s.mode) {
|
||||
case MODE_TITLE:
|
||||
cmd = "title @a title " + json;
|
||||
break;
|
||||
case MODE_SUBTITLE:
|
||||
cmd = "title @a subtitle " + json;
|
||||
break;
|
||||
default:
|
||||
cmd = "tellraw @a " + json;
|
||||
break;
|
||||
}
|
||||
p.closeInventory();
|
||||
// 以玩家自己身份执行:权限模型与原版指令完全一致
|
||||
boolean ok = false;
|
||||
try {
|
||||
ok = Bukkit.dispatchCommand(p, cmd);
|
||||
} catch (Throwable t) {
|
||||
plugin.getLogger().warning(
|
||||
"[JSON构造器] 指令执行异常: " + t.getMessage());
|
||||
}
|
||||
if (ok) {
|
||||
p.sendMessage("§a[JSON构造器] §7已发送("
|
||||
+ (s.mode == MODE_TITLE ? "大标题"
|
||||
: s.mode == MODE_SUBTITLE ? "小标题" : "tellraw消息") + ")");
|
||||
} else {
|
||||
p.sendMessage("§c[JSON构造器] 发送失败:权限不足或指令被拒绝");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本(含 § 颜色码)→ tellraw JSON。
|
||||
* § 颜色切段:每段一个 extra 元素;§x hex 保留为 color:#hex;
|
||||
* §l/§o/§n/§m/§k/§r 样式码透传为 bold/italic/underlined/strikethrough/obfuscated/reset。
|
||||
*/
|
||||
static String toTellrawJson(String text) {
|
||||
StringBuilder out = new StringBuilder("{\"text\":\"\",\"extra\":[");
|
||||
boolean first = true;
|
||||
|
||||
List<String[]> parts = new ArrayList<>(); // {text,color,bits}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String color = null;
|
||||
int flags = 0; // 1 bold 2 italic 4 under 8 strike 16 obf
|
||||
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char c = text.charAt(i);
|
||||
if (c == '§' && i + 1 < text.length()) {
|
||||
char code = Character.toLowerCase(text.charAt(++i));
|
||||
if (sb.length() > 0) {
|
||||
parts.add(new String[]{sb.toString(), color, String.valueOf(flags)});
|
||||
sb.setLength(0);
|
||||
}
|
||||
switch (code) {
|
||||
case '0': color = "black"; flags = 0; break;
|
||||
case '1': color = "dark_blue"; flags = 0; break;
|
||||
case '2': color = "dark_green"; flags = 0; break;
|
||||
case '3': color = "dark_aqua"; flags = 0; break;
|
||||
case '4': color = "dark_red"; flags = 0; break;
|
||||
case '5': color = "dark_purple"; flags = 0; break;
|
||||
case '6': color = "gold"; flags = 0; break;
|
||||
case '7': color = "gray"; flags = 0; break;
|
||||
case '8': color = "dark_gray"; flags = 0; break;
|
||||
case '9': color = "blue"; flags = 0; break;
|
||||
case 'a': color = "green"; flags = 0; break;
|
||||
case 'b': color = "aqua"; flags = 0; break;
|
||||
case 'c': color = "red"; flags = 0; break;
|
||||
case 'd': color = "light_purple"; flags = 0; break;
|
||||
case 'e': color = "yellow"; flags = 0; break;
|
||||
case 'f': color = "white"; flags = 0; break;
|
||||
case 'x': {
|
||||
// §x§R§R§G§G§B§B
|
||||
StringBuilder hx = new StringBuilder("#");
|
||||
while (i + 2 < text.length()
|
||||
&& text.charAt(i + 1) == '§'
|
||||
&& isHexChar(text.charAt(i + 2))) {
|
||||
hx.append(text.charAt(i + 2));
|
||||
i += 2;
|
||||
}
|
||||
color = hx.length() == 7 ? hx.toString() : color;
|
||||
break;
|
||||
}
|
||||
case 'l': flags |= 1; break;
|
||||
case 'o': flags |= 2; break;
|
||||
case 'n': flags |= 4; break;
|
||||
case 'm': flags |= 8; break;
|
||||
case 'k': flags |= 16; break;
|
||||
case 'r': color = null; flags = 0; break;
|
||||
default: break; // 未知码忽略
|
||||
}
|
||||
} else if (c == '\n') {
|
||||
sb.append("\\n");
|
||||
} else if (c == '"' || c == '\\') {
|
||||
// JSON 字符串转义,防玩家输入引号/反斜杠打坏整条指令
|
||||
sb.append('\\').append(c);
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
if (sb.length() > 0) {
|
||||
parts.add(new String[]{sb.toString(), color, String.valueOf(flags)});
|
||||
}
|
||||
|
||||
for (String[] seg : parts) {
|
||||
if (!first) out.append(',');
|
||||
first = false;
|
||||
out.append("{\"text\":\"").append(seg[0]).append("\"");
|
||||
if (seg[1] != null) out.append(",\"color\":\"").append(seg[1]).append('"');
|
||||
int f = Integer.parseInt(seg[2]);
|
||||
if ((f & 1) != 0) out.append(",\"bold\":true");
|
||||
if ((f & 2) != 0) out.append(",\"italic\":true");
|
||||
if ((f & 4) != 0) out.append(",\"underlined\":true");
|
||||
if ((f & 8) != 0) out.append(",\"strikethrough\":true");
|
||||
if ((f & 16) != 0) out.append(",\"obfuscated\":true");
|
||||
out.append('}');
|
||||
}
|
||||
out.append("]}");
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private static boolean isHexChar(char c) {
|
||||
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
|
||||
}
|
||||
}
|
||||
@@ -191,6 +191,10 @@ public class Main extends JavaPlugin
|
||||
public AchievementEffectGuard achievementEffectGuard;
|
||||
public LandRecordManager landRecordManager;
|
||||
public InventoryAutoBackup inventoryAutoBackup;
|
||||
/** 26批:箱子搬运(精准采集斧拆箱→2秒窗口打包→放置回填) */
|
||||
public ChestCarryManager chestCarryManager;
|
||||
/** 26批:/json JSON消息构造器(tellraw/大标题/小标题 + 颜色 + 报错接管) */
|
||||
public JsonMessageGui jsonGui;
|
||||
private TeleportManager teleportMgr;
|
||||
private HomeManager homeMgr;
|
||||
private DeathReport deathReport;
|
||||
@@ -388,6 +392,16 @@ public class Main extends JavaPlugin
|
||||
saveResource("illegal_domains.txt", false);
|
||||
getLogger().info("[配置] 已自动释放 illegal_domains.txt 到插件目录");
|
||||
}
|
||||
// ★ 26批:自动释放 json解码配置/解码配置.md(调色板自定义色,仅不存在时)
|
||||
try {
|
||||
File jsonCfg = new File(getDataFolder(), "json解码配置/解码配置.md");
|
||||
if (!jsonCfg.exists()) {
|
||||
saveResource("json解码配置/解码配置.md", false);
|
||||
getLogger().info("[配置] 已自动释放 json解码配置/解码配置.md");
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
getLogger().warning("[配置] 解码配置.md 释放失败: " + t.getMessage());
|
||||
}
|
||||
// ★ 任务94:根目录5类文件归位(必须在任何 Manager 打开文件/DB 之前)
|
||||
migrateRootDataFiles();
|
||||
// 启动时清理.sdf1临时文件
|
||||
@@ -674,6 +688,14 @@ public class Main extends JavaPlugin
|
||||
areaGUIManager = new AreaGUIManager(this, areaProtection);
|
||||
getServer().getPluginManager()
|
||||
.registerEvents(areaGUIManager, this);
|
||||
// 26批:箱子搬运(依赖 areaProtection 权限链)
|
||||
chestCarryManager = new ChestCarryManager(this);
|
||||
getServer().getPluginManager()
|
||||
.registerEvents(chestCarryManager, this);
|
||||
// 26批:/json JSON消息构造器(含解码配置.md 调色板)
|
||||
jsonGui = new JsonMessageGui(this);
|
||||
getServer().getPluginManager()
|
||||
.registerEvents(jsonGui, this);
|
||||
// 区域防护CLI
|
||||
areaCLIManager = new AreaCLIManager(this, areaProtection);
|
||||
|
||||
@@ -703,16 +725,37 @@ public class Main extends JavaPlugin
|
||||
// 15.1.1 ====MC官方中文翻译加载(异步下载 zh_cn.json)====
|
||||
OfficialTranslations.init(this);
|
||||
|
||||
// 15.2 ====背包滚动自动备份(每15分钟)====
|
||||
// 15.2 ====背包滚动自动备份(每小时 0/15/30/45 分准点)====
|
||||
inventoryAutoBackup = new InventoryAutoBackup(this);
|
||||
getServer().getScheduler().runTaskTimer(this,
|
||||
() -> {
|
||||
if (inventoryAutoBackup != null) {
|
||||
inventoryAutoBackup.runBackup();
|
||||
}
|
||||
},
|
||||
InventoryAutoBackup.INTERVAL_TICKS, // 首跑延迟 15 分钟
|
||||
InventoryAutoBackup.INTERVAL_TICKS); // 之后每 15 分钟
|
||||
// ★ 第26批:改准点备份。原 runTaskTimer 从【启动时刻】起算 15 分钟周期,
|
||||
// 落点永远带启动偏移(如 10:07/10:22/10:37…)。改为墙钟自对齐、每轮
|
||||
// 执行完按【执行时刻】重排下一个 900000ms 边界的 runTaskLater
|
||||
// (同 OpWhiteListManager.scheduleAligned 范式),无论 TPS 高低、服务器
|
||||
// 何时启动,都精确落在每小时第 0/15/30/45 分(period=900000ms=15 分钟)。
|
||||
final Runnable[] invBackupTick = new Runnable[1];
|
||||
invBackupTick[0] = () -> {
|
||||
try {
|
||||
if (inventoryAutoBackup != null) {
|
||||
inventoryAutoBackup.runBackup();
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
getLogger().warning("[自动备份] 定时任务异常(下轮照常): " + t.getMessage());
|
||||
} finally {
|
||||
if (isEnabled()) {
|
||||
long now = System.currentTimeMillis();
|
||||
long next = (now / 900000L + 1) * 900000L;
|
||||
getServer().getScheduler().runTaskLater(this, invBackupTick[0],
|
||||
Math.max(1L, (next - now + 49L) / 50L));
|
||||
}
|
||||
}
|
||||
};
|
||||
// 首跑:同样对齐到下一个 0/15/30/45 分墙钟边界(不再等固定 15 分钟)
|
||||
{
|
||||
long now = System.currentTimeMillis();
|
||||
long next = (now / 900000L + 1) * 900000L;
|
||||
getServer().getScheduler().runTaskLater(this, invBackupTick[0],
|
||||
Math.max(1L, (next - now + 49L) / 50L));
|
||||
}
|
||||
|
||||
// 15.3 ====领地记录每日清理(保留 7 天)====
|
||||
// 每天 0 点附近跑一次:用 24h 周期,首跑延迟到下一个整点对齐不强制,
|
||||
@@ -4465,6 +4508,21 @@ public class Main extends JavaPlugin
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- 1.5) 原版 title/tellraw 报错 -> /json 构造器接管并提取用户原文 ----
|
||||
//(26批 任务170:玩家 JSON 写错时不再干看原版报错,直接拉起可视化编辑器)
|
||||
if (isPlayer && jsonGui != null) {
|
||||
String lowerBody = body.toLowerCase(java.util.Locale.ROOT);
|
||||
if (lowerBody.startsWith("tellraw ")
|
||||
|| lowerBody.startsWith("title ")) {
|
||||
if (jsonGui.tryTakeover((Player) sender, body)) {
|
||||
suppressUnknown(e);
|
||||
getLogger().info("[无感切换] title/tellraw 报错已接管进JSON构造器: \""
|
||||
+ body + "\"");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 2) 指令根存在,但参数不全/语法写错 -> 中文友好报错 ----
|
||||
//(2026-10-04 新功能,详见 buildFriendlySyntaxError 注释)
|
||||
if (looksLikeCommandAttempt(body)) {
|
||||
@@ -8876,6 +8934,19 @@ public class Main extends JavaPlugin
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===== /json:JSON消息构造器(tellraw/大标题/小标题 + 颜色)=====
|
||||
// 全员可用:发送以玩家自身身份执行原版指令,权限天然受原版约束
|
||||
if (cmdName.equals("json")) {
|
||||
if (!(sender instanceof Player)) {
|
||||
sender.sendMessage("§c仅玩家可用");
|
||||
return true;
|
||||
}
|
||||
if (jsonGui != null) {
|
||||
jsonGui.open((Player) sender);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// /垃圾清理
|
||||
if (cmdName.equals("垃圾清理")) {
|
||||
@@ -9601,7 +9672,7 @@ public class Main extends JavaPlugin
|
||||
}
|
||||
}
|
||||
if (!autoBks.isEmpty()) {
|
||||
sender.sendMessage("§6[自动备份](游戏中每15分钟保存)");
|
||||
sender.sendMessage("§6[自动备份](每小时 0/15/30/45 分准点保存)");
|
||||
for (Map<String, Object> b : autoBks) {
|
||||
int id = ((Number) b.get("id")).intValue();
|
||||
long st = b.get("save_time") != null
|
||||
@@ -9749,7 +9820,7 @@ public class Main extends JavaPlugin
|
||||
|
||||
// 自动备份
|
||||
if (!autoList.isEmpty()) {
|
||||
sender.sendMessage("§6[自动备份](游戏中每15分钟保存)");
|
||||
sender.sendMessage("§6[自动备份](每小时 0/15/30/45 分准点保存)");
|
||||
for (Map<String, Object> b : autoList) {
|
||||
int bidi = ((Number) b.get("id")).intValue();
|
||||
long st = b.get("save_time") != null
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
## ----配置区域开始----
|
||||
|
||||
| 颜色代码 |
|
||||
| --- |
|
||||
| red |
|
||||
| aqua |
|
||||
| |
|
||||
|
||||
## ----配置区域结束----
|
||||
|
||||
## json解码规则说明
|
||||
|
||||
### 一、怎么触发?
|
||||
使用`json`指令触发,或在原版`title、tellraw`产生报错时,插件介入。
|
||||
### 二、颜色代码怎么配置?
|
||||
支持填入`16进制`颜色代码(比如#000000)、或者颜色`英文名`(比如red)、也可以是rgb。比如(R0 G0 B0)
|
||||
@@ -193,3 +193,6 @@ commands:
|
||||
cleartake:
|
||||
description: 清空DB队列中未回应的web同步任务(管理员)
|
||||
usage: /cleartake
|
||||
json:
|
||||
description: JSON消息构造器(tellraw/大标题/小标题+颜色)
|
||||
usage: /json
|
||||
|
||||
Reference in New Issue
Block a user