ChatMenuPlus.java v1.0
聊天界面玩家名可点击,弹出可配置操作菜单,含临时屏蔽功能(支持Paper 1.21.1及以上)。
命令列表
- chatmenu重载菜单配置
package com.scriptirc.chatmenu;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.CommandExecutor;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerChatEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.plugin.java.JavaPlugin;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.io.*;
/**
* @pluginName ChatMenuPlus
* @author 2a2t.org
* @version 1.0
* @description 聊天界面玩家名可点击,弹出可配置操作菜单,含临时屏蔽功能(支持Paper 1.21.1及以上)。
* [command]chatmenu|重载菜单配置[/command]
* [Permission]chatmenu.admin|重载菜单配置权限[/Permission]
*/
public class ChatMenuPlus extends JavaPlugin implements Listener, CommandExecutor {
// 菜单配置项结构
public static class MenuAction {
public String name; // 显示文本
public String command; // 执行命令(可用变量{player}、{target})
public String type; // 类型(run/suggest/ignore)
public MenuAction(String name, String command, String type) {
this.name = name;
this.command = command;
this.type = type;
}
}
// 菜单配置(内存缓存)
private List<MenuAction> menuConfig = new ArrayList<>();
// 临时屏蔽数据 key=操作人,value=被屏蔽玩家名集合
private final Map<UUID, Set<String>> ignoreMap = new ConcurrentHashMap<>();
// 配置文件路径
private File configFile;
@Override
public void onEnable() {
Bukkit.getPluginManager().registerEvents(this, this);
this.getCommand("chatmenu").setExecutor(this);
configFile = new File(getDataFolder(), "menu.json");
if (!getDataFolder().exists()) {
boolean created = getDataFolder().mkdirs();
if (!created) {
getLogger().warning("无法创建插件数据目录!配置文件可能无法保存。");
}
}
loadMenuConfig();
getLogger().info("ChatMenuPlus (adventure) 启动成功!");
}
@Override
public void onDisable() {
ignoreMap.clear();
}
// 聊天事件拦截,替换玩家名为可点击 adventure 组件
@EventHandler
public void onChat(PlayerChatEvent event) {
Player sender = event.getPlayer();
String senderName = sender.getName();
String message = event.getMessage();
// 过滤被屏蔽
event.getRecipients().removeIf(p -> {
Set<String> ignores = ignoreMap.get(p.getUniqueId());
return ignores != null && ignores.contains(senderName);
});
// 构造可点击玩家名组件
Component nameComp = Component.text(senderName)
.color(NamedTextColor.AQUA)
.decorate(TextDecoration.BOLD)
.hoverEvent(HoverEvent.showText(Component.text("点击操作")))
.clickEvent(ClickEvent.runCommand("/chatmenu show " + senderName));
// 构造消息体
Component msgComp = Component.text(": " + message).color(NamedTextColor.WHITE);
// 组装
Component full = Component.empty().append(nameComp).append(msgComp);
// 发送给所有未屏蔽的接收者
for (Player p : event.getRecipients()) {
p.sendMessage(full);
}
// 取消原始广播
event.setCancelled(true);
}
// 临时屏蔽失效:玩家退出时清理
@EventHandler
public void onQuit(PlayerQuitEvent event) {
ignoreMap.remove(event.getPlayer().getUniqueId());
}
// /chatmenu 命令处理
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (args.length == 0) {
sender.sendMessage(Component.text("用法: /chatmenu reload | show <玩家名>").color(NamedTextColor.YELLOW));
return true;
}
if (args[0].equalsIgnoreCase("reload")) {
if (!sender.hasPermission("chatmenu.admin")) {
sender.sendMessage(Component.text("没有权限").color(NamedTextColor.RED));
return true;
}
loadMenuConfig();
sender.sendMessage(Component.text("菜单配置已重载").color(NamedTextColor.GREEN));
return true;
}
if (args[0].equalsIgnoreCase("show") && args.length >= 2) {
if (!(sender instanceof Player)) {
sender.sendMessage(Component.text("只能玩家使用").color(NamedTextColor.RED));
return true;
}
Player p = (Player) sender;
String target = args[1];
sendMenuToPlayer(p, target);
return true;
}
sender.sendMessage(Component.text("用法: /chatmenu reload | show <玩家名>").color(NamedTextColor.YELLOW));
return true;
}
// 发送菜单消息(adventure组件)
private void sendMenuToPlayer(Player player, String targetName) {
Component menu = Component.text("对 " + targetName + " 可用操作: ", NamedTextColor.GOLD);
for (MenuAction act : menuConfig) {
Component btn = Component.text("[" + act.name + "]")
.color(NamedTextColor.GREEN)
.decorate(TextDecoration.BOLD)
.hoverEvent(HoverEvent.showText(Component.text("点击执行: " + act.name)));
String cmd = act.command.replace("{player}", player.getName()).replace("{target}", targetName);
if ("run".equalsIgnoreCase(act.type)) {
btn = btn.clickEvent(ClickEvent.runCommand(cmd));
} else if ("suggest".equalsIgnoreCase(act.type)) {
btn = btn.clickEvent(ClickEvent.suggestCommand(cmd));
} else if ("ignore".equalsIgnoreCase(act.type)) {
btn = btn.clickEvent(ClickEvent.runCommand("/chatmenu ignore " + targetName));
} else {
btn = btn.clickEvent(ClickEvent.suggestCommand(cmd));
}
menu = menu.append(Component.space()).append(btn);
}
player.sendMessage(menu);
}
// 监听命令执行(屏蔽)
@EventHandler
public void onPlayerCommand(PlayerCommandPreprocessEvent event) {
String msg = event.getMessage();
if (msg.startsWith("/chatmenu ignore ")) {
String[] arr = msg.split(" ", 3);
if (arr.length < 3) return;
String target = arr[2];
Player p = event.getPlayer();
ignoreMap.computeIfAbsent(p.getUniqueId(), k -> new HashSet<>()).add(target);
p.sendMessage(Component.text("你已临时屏蔽 " + target + " 的发言(重登失效)", NamedTextColor.GRAY));
event.setCancelled(true);
}
}
// 加载菜单配置
private void loadMenuConfig() {
menuConfig.clear();
if (!configFile.exists()) {
// 默认菜单
menuConfig.add(new MenuAction("请求传送", "/tpa {target}", "run"));
menuConfig.add(new MenuAction("私聊", "/msg {target} ", "suggest"));
menuConfig.add(new MenuAction("屏蔽", "", "ignore"));
saveMenuConfig();
return;
}
try (BufferedReader reader = new BufferedReader(new FileReader(configFile))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) sb.append(line);
String json = sb.toString();
// 简单解析(无第三方库)
json = json.replace("[","").replace("]","");
String[] items = json.split("},");
for (String item : items) {
item = item.replace("{","").replace("}","");
String[] fields = item.split(",");
String name = "", cmd = "", type = "run";
for (String f : fields) {
String[] kv = f.split(":",2);
if (kv.length < 2) continue;
String k = kv[0].replace("\"","").trim();
String v = kv[1].replace("\"","").trim();
if (k.equals("name")) name = v;
else if (k.equals("command")) cmd = v;
else if (k.equals("type")) type = v;
}
if (!name.isEmpty()) menuConfig.add(new MenuAction(name, cmd, type));
}
} catch (Exception e) {
getLogger().warning("菜单配置加载失败,使用默认菜单");
menuConfig.clear();
menuConfig.add(new MenuAction("请求传送", "/tpa {target}", "run"));
menuConfig.add(new MenuAction("私聊", "/msg {target} ", "suggest"));
menuConfig.add(new MenuAction("屏蔽", "", "ignore"));
}
}
// 保存菜单配置
private void saveMenuConfig() {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(configFile))) {
writer.write("[");
boolean first = true;
for (MenuAction act : menuConfig) {
if (!first) writer.write(",");
writer.write(String.format("{\"name\":\"%s\",\"command\":\"%s\",\"type\":\"%s\"}",
act.name.replace("\"", ""), act.command.replace("\"", ""), act.type.replace("\"", "")));
first = false;
}
writer.write("]");
} catch (Exception e) {
getLogger().warning("菜单配置保存失败");
}
}
}