9 Commits
Author SHA1 Message Date
Coldsmile 292b93e842 fix: 修复 1.3.0 实机测试问题
Build / build (push) Has been cancelled
Build / Paper 1.21.11 / Java 21 (push) Has been cancelled
Build / Paper 26.1.2 / Java 25 (push) Has been cancelled
Build / Paper 26.2 / Java 25 (push) Has been cancelled
2026-08-05 00:42:24 +08:00
Coldsmile 7816b75325 feat: 准备 1.3.0 稳定版本 2026-08-02 12:18:04 +08:00
Coldsmile 92ef7f6030 feat: 完善稳定性并重构潜影盒安全机制 2026-08-02 03:36:19 +08:00
Coldsmile d9b1312e0c feat: 完善插件稳定性与兼容性 2026-08-01 00:36:17 +08:00
Coldsmile f8f731f927 feat: 完善 TPA 并整理插件配置 2026-07-31 19:07:50 +08:00
Coldsmile 66bf93b10d merge: 合并 SkinBridge 皮肤兼容功能 2026-07-21 23:45:55 +08:00
Coldsmile d6c8db6019 feat: 添加 SkinBridge 皮肤兼容 2026-07-21 23:45:19 +08:00
Coldsmile 5d86f0e3eb feat: 完整实现 TPA 并添加多版本适配 2026-06-25 23:40:30 +08:00
Coldsmile 061c3be2b1 feat: 完善维护模式与运行期功能管理 2026-06-12 00:08:40 +08:00
19 changed files with 284 additions and 48 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# 更新日志 # 更新日志
## 1.4.0 - 2026-08-02 ## 1.3.0 - 2026-08-02
### 新增 ### 新增
+9
View File
@@ -45,6 +45,15 @@
- `/essc admin` 管理模式切换 - `/essc admin` 管理模式切换
- `/maintenance` `(/maint)` 维护模式管理 - `/maintenance` `(/maint)` 维护模式管理
### 控制台命令
- `/essc help``/essc version``/essc reload`
- `/maintenance [on|off|status|reload|add|remove|list]`,也可通过 `/essc maintenance ...` 执行
- `/seen [玩家]``/heal [玩家]``/feed [玩家]``/tpsbar [玩家]`
- `/essc skin [status|refresh] [玩家]`
依赖发送者位置、背包或玩家交互状态的命令仅允许在游戏内执行。控制台执行玩家状态命令时必须明确提供目标玩家。
### 其它功能 ### 其它功能
- Shift + 右键快捷打开潜影盒 - Shift + 右键快捷打开潜影盒
+1 -1
View File
@@ -10,7 +10,7 @@ plugins {
} }
group = 'cn.infstar' group = 'cn.infstar'
version = providers.gradleProperty('pluginVersion').orElse('1.4.0').get() version = providers.gradleProperty('pluginVersion').orElse('1.3.0').get()
repositories { repositories {
mavenCentral() mavenCentral()
+1 -1
View File
@@ -31,7 +31,7 @@ printf 'online-mode=false\nserver-port=0\nenable-query=false\n' > "$workspace/se
server_pid=$! server_pid=$!
for _ in $(seq 1 120); do for _ in $(seq 1 120); do
if grep -Fq 'EssentialsC v1.4.0 已启用' "$workspace/server.log"; then if grep -Fq 'EssentialsC v1.3.0 已启用' "$workspace/server.log"; then
if grep -Eq 'UnsupportedClassVersionError|Error occurred while enabling EssentialsC|Could not load.*EssentialsC' "$workspace/server.log"; then if grep -Eq 'UnsupportedClassVersionError|Error occurred while enabling EssentialsC|Could not load.*EssentialsC' "$workspace/server.log"; then
cat "$workspace/server.log" cat "$workspace/server.log"
exit 1 exit 1
@@ -448,7 +448,8 @@ public final class EssentialsC extends JavaPlugin {
@Override @Override
public Collection<String> suggest(CommandSourceStack commandSourceStack, String[] args) { public Collection<String> suggest(CommandSourceStack commandSourceStack, String[] args) {
if (executor instanceof TabCompleter completer) { if (executor instanceof TabCompleter completer) {
return completer.onTabComplete(commandSourceStack.getSender(), commandAdapter, name, args); String[] completionArgs = args.length == 0 ? new String[]{""} : args;
return completer.onTabComplete(commandSourceStack.getSender(), commandAdapter, name, completionArgs);
} }
return List.of(); return List.of();
} }
@@ -21,6 +21,9 @@ import java.util.regex.Pattern;
public class LangManager { public class LangManager {
private static final Pattern HEX_COLOR_PATTERN = Pattern.compile("(?i)&#([0-9a-f]{6})"); private static final Pattern HEX_COLOR_PATTERN = Pattern.compile("(?i)&#([0-9a-f]{6})");
private static final Pattern MINI_MESSAGE_TAG_PATTERN = Pattern.compile(
"(?<!\\\\)(</?)([A-Za-z][A-Za-z0-9_-]*)(?=[:>])"
);
private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage();
private static final LegacyComponentSerializer LEGACY_AMPERSAND = LegacyComponentSerializer.legacyAmpersand(); private static final LegacyComponentSerializer LEGACY_AMPERSAND = LegacyComponentSerializer.legacyAmpersand();
private static final LegacyComponentSerializer LEGACY_SECTION = LegacyComponentSerializer.legacySection(); private static final LegacyComponentSerializer LEGACY_SECTION = LegacyComponentSerializer.legacySection();
@@ -161,18 +164,25 @@ public class LangManager {
langFile.setDefaults(selectedDefaults); langFile.setDefaults(selectedDefaults);
} }
private String applyPlaceholders(String value, Map<String, String> placeholders) { static String applyPlaceholders(String value, Map<String, String> placeholders) {
String result = value; String result = value;
boolean miniMessage = looksLikeMiniMessage(value);
for (Map.Entry<String, String> entry : placeholders.entrySet()) { for (Map.Entry<String, String> entry : placeholders.entrySet()) {
result = result.replace("{" + entry.getKey() + "}", escapeMiniMessageReplacement(entry.getValue())); String replacement = miniMessage
? escapeMiniMessageReplacement(entry.getValue())
: entry.getValue();
result = result.replace("{" + entry.getKey() + "}", replacement == null ? "" : replacement);
} }
return result; return result;
} }
private String escapeMiniMessageReplacement(String value) { static String escapeMiniMessageReplacement(String value) {
if (value == null) { if (value == null) {
return ""; return "";
} }
if (value.indexOf('§') >= 0) {
return MINI_MESSAGE.serialize(LEGACY_SECTION.deserialize(value));
}
return value.replace("\\", "\\\\").replace("<", "\\<"); return value.replace("\\", "\\\\").replace("<", "\\<");
} }
@@ -213,7 +223,7 @@ public class LangManager {
} }
if (looksLikeMiniMessage(text)) { if (looksLikeMiniMessage(text)) {
try { try {
return MINI_MESSAGE.deserialize(text); return MINI_MESSAGE.deserialize(normalizeMiniMessageTags(text));
} catch (RuntimeException ignored) { } catch (RuntimeException ignored) {
// 配置中 MiniMessage 语法错误时回退到旧颜色码解析,避免消息完全不可用。 // 配置中 MiniMessage 语法错误时回退到旧颜色码解析,避免消息完全不可用。
} }
@@ -221,8 +231,20 @@ public class LangManager {
return LEGACY_SECTION.deserialize(translateColorCodes(text)); return LEGACY_SECTION.deserialize(translateColorCodes(text));
} }
private boolean looksLikeMiniMessage(String text) { private static boolean looksLikeMiniMessage(String text) {
int open = text.indexOf('<'); int open = text.indexOf('<');
return open >= 0 && text.indexOf('>', open) > open; return open >= 0 && text.indexOf('>', open) > open;
} }
static String normalizeMiniMessageTags(String text) {
Matcher matcher = MINI_MESSAGE_TAG_PATTERN.matcher(text);
StringBuilder result = new StringBuilder();
while (matcher.find()) {
matcher.appendReplacement(result, Matcher.quoteReplacement(
matcher.group(1) + matcher.group(2).toLowerCase(java.util.Locale.ROOT)
));
}
matcher.appendTail(result);
return result.toString();
}
} }
@@ -49,12 +49,17 @@ public abstract class BaseCommand implements CommandExecutor {
@Override @Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (sender instanceof Player player) { return dispatch(sender, args);
if (!player.hasPermission(permission)) { }
player.sendMessage(getLang().getPrefixedString("messages.no-permission",
final boolean dispatch(CommandSender sender, String[] args) {
if (!sender.hasPermission(permission)) {
sender.sendMessage(getLang().getPrefixedString("messages.no-permission",
Map.of("permission", permission))); Map.of("permission", permission)));
return true; return true;
} }
if (sender instanceof Player player) {
return execute(player, args); return execute(player, args);
} }
@@ -1,6 +1,7 @@
package cn.infstar.essentialsC.commands; package cn.infstar.essentialsC.commands;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import java.util.Map; import java.util.Map;
@@ -36,6 +37,30 @@ public class FeedCommand extends BaseCommand {
return true; return true;
} }
@Override
protected boolean executeConsole(CommandSender sender, String[] args) {
if (args.length != 1) {
sender.sendMessage(getLang().getPrefixedString("messages.feed-usage-console"));
return true;
}
if (!sender.hasPermission("essentialsc.command.feed.others")) {
sender.sendMessage(getLang().getPrefixedString("messages.no-permission-others"));
return true;
}
Player target = Bukkit.getPlayerExact(args[0]);
if (target == null) {
sender.sendMessage(getLang().getPrefixedString("messages.player-not-found", Map.of("player", args[0])));
return true;
}
feedPlayer(target);
sender.sendMessage(getLang().getPrefixedString("messages.feed-other", Map.of("player", target.getName())));
target.sendMessage(getLang().getPrefixedString("messages.feed-by-other",
Map.of("admin", getLang().getString("messages.console-name"))));
return true;
}
private void feedPlayer(Player player) { private void feedPlayer(Player player) {
player.setFoodLevel(20); player.setFoodLevel(20);
player.setSaturation(20f); player.setSaturation(20f);
@@ -3,6 +3,7 @@ package cn.infstar.essentialsC.commands;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.attribute.Attribute; import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance; import org.bukkit.attribute.AttributeInstance;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import java.util.Map; import java.util.Map;
@@ -38,6 +39,30 @@ public class HealCommand extends BaseCommand {
return true; return true;
} }
@Override
protected boolean executeConsole(CommandSender sender, String[] args) {
if (args.length != 1) {
sender.sendMessage(getLang().getPrefixedString("messages.heal-usage-console"));
return true;
}
if (!sender.hasPermission("essentialsc.command.heal.others")) {
sender.sendMessage(getLang().getPrefixedString("messages.no-permission-others"));
return true;
}
Player target = Bukkit.getPlayerExact(args[0]);
if (target == null) {
sender.sendMessage(getLang().getPrefixedString("messages.player-not-found", Map.of("player", args[0])));
return true;
}
healPlayer(target);
sender.sendMessage(getLang().getPrefixedString("messages.heal-other", Map.of("player", target.getName())));
target.sendMessage(getLang().getPrefixedString("messages.heal-by-other",
Map.of("admin", getLang().getString("messages.console-name"))));
return true;
}
private void healPlayer(Player player) { private void healPlayer(Player player) {
AttributeInstance maxHealth = player.getAttribute(Attribute.MAX_HEALTH); AttributeInstance maxHealth = player.getAttribute(Attribute.MAX_HEALTH);
player.setHealth(maxHealth == null ? player.getHealth() : maxHealth.getValue()); player.setHealth(maxHealth == null ? player.getHealth() : maxHealth.getValue());
@@ -38,7 +38,12 @@ public class HelpCommand extends BaseCommand implements TabCompleter {
@Override @Override
protected boolean executeConsole(CommandSender sender, String[] args) { protected boolean executeConsole(CommandSender sender, String[] args) {
if (args.length > 0 && args[0].equalsIgnoreCase("reload")) { if (args.length == 0 || args[0].equalsIgnoreCase("help")) {
sendConsoleHelp(sender);
return true;
}
if (args[0].equalsIgnoreCase("reload")) {
if (!sender.hasPermission("essentialsc.command.reload")) { if (!sender.hasPermission("essentialsc.command.reload")) {
sendNoPermission(sender, "essentialsc.command.reload"); sendNoPermission(sender, "essentialsc.command.reload");
return true; return true;
@@ -51,10 +56,62 @@ public class HelpCommand extends BaseCommand implements TabCompleter {
sender.sendMessage(getLang().getPrefixedString("messages.config-reloaded")); sender.sendMessage(getLang().getPrefixedString("messages.config-reloaded"));
return true; return true;
} }
sender.sendMessage(getLang().getPrefixedString("messages.player-only"));
if (args[0].equalsIgnoreCase("version") || args[0].equalsIgnoreCase("v")) {
sender.sendMessage(getLang().getPrefixedString("messages.version",
Map.of("version", plugin.getPluginMeta().getVersion())));
sender.sendMessage(getLang().getPrefixedString("messages.paper-version",
Map.of("version", Bukkit.getVersion())));
return true; return true;
} }
String actualCommand = getActualCommand(args[0]);
BaseCommand targetCommand = CommandRegistry.getCommand(actualCommand);
if (actualCommand != null && targetCommand != null) {
return targetCommand.dispatch(sender, Arrays.copyOfRange(args, 1, args.length));
}
sender.sendMessage(getLang().getPrefixedString("messages.unknown-subcommand",
Map.of("command", args[0])));
sender.sendMessage(getLang().getPrefixedString("messages.help-usage"));
return true;
}
private void sendConsoleHelp(CommandSender sender) {
LangManager lang = getLang();
sendPrefixed(sender, lang.getString("help.title"));
sendPrefixed(sender, lang.getString("help.version",
Map.of("version", plugin.getPluginMeta().getVersion())));
sender.sendMessage("");
sendPrefixed(sender, lang.getString("help.section-other"));
if (sender.hasPermission("essentialsc.command.reload")) {
sendPrefixed(sender, lang.getString("help.commands.reload"));
}
sendPrefixed(sender, lang.getString("help.commands.version"));
if (CommandRegistry.isAvailable("seen") && sender.hasPermission("essentialsc.command.seen")) {
sendPrefixed(sender, lang.getString("help.commands.seen"));
}
if (CommandRegistry.isAvailable("heal") && sender.hasPermission("essentialsc.command.heal")) {
sendPrefixed(sender, lang.getString("help.commands.heal"));
}
if (CommandRegistry.isAvailable("feed") && sender.hasPermission("essentialsc.command.feed")) {
sendPrefixed(sender, lang.getString("help.commands.feed"));
}
if (CommandRegistry.isAvailable("skin") && sender.hasPermission("essentialsc.command.skin")) {
sendPrefixed(sender, lang.getString("help.commands.skin"));
}
if (CommandRegistry.isAvailable("tpsbar") && sender.hasPermission("essentialsc.command.tpsbar")) {
sendPrefixed(sender, lang.getString("help.commands.tpsbar"));
}
if (CommandRegistry.isAvailable("maintenance")
&& sender.hasPermission("essentialsc.command.maintenance")) {
sendPrefixed(sender, lang.getString("help.commands.maintenance"));
}
sender.sendMessage("");
sendPrefixed(sender, lang.getString("help.footer"));
}
private boolean handleCommand(CommandSender sender, Player player, String[] args) { private boolean handleCommand(CommandSender sender, Player player, String[] args) {
if (args.length > 0) { if (args.length > 0) {
String subCommand = args[0].toLowerCase(); String subCommand = args[0].toLowerCase();
@@ -246,7 +303,7 @@ public class HelpCommand extends BaseCommand implements TabCompleter {
} }
private void sendPrefixed(CommandSender sender, String message) { private void sendPrefixed(CommandSender sender, String message) {
sender.sendMessage(message); sender.sendMessage(getLang().getPrefix() + message);
} }
private void sendPrefixedLines(CommandSender sender, String message) { private void sendPrefixedLines(CommandSender sender, String message) {
@@ -2,6 +2,7 @@ package cn.infstar.essentialsC.commands;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer; import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
@@ -16,45 +17,56 @@ public class SeenCommand extends BaseCommand {
@Override @Override
protected boolean execute(Player player, String[] args) { protected boolean execute(Player player, String[] args) {
return executeCommand(player, player, args);
}
@Override
protected boolean executeConsole(CommandSender sender, String[] args) {
return executeCommand(sender, null, args);
}
private boolean executeCommand(CommandSender sender, Player viewer, String[] args) {
if (args.length == 0) { if (args.length == 0) {
player.sendMessage(getLang().getPrefixedString("messages.seen-usage")); sender.sendMessage(getLang().getPrefixedString(viewer == null
? "messages.seen-usage-console"
: "messages.seen-usage"));
return true; return true;
} }
Player onlineTarget = Bukkit.getPlayerExact(args[0]); Player onlineTarget = Bukkit.getPlayerExact(args[0]);
if (onlineTarget != null && VanishCommand.isVanished(onlineTarget) if (onlineTarget != null && VanishCommand.isVanished(onlineTarget)
&& !player.hasPermission(VanishCommand.SEE_PERMISSION)) { && viewer != null && !viewer.hasPermission(VanishCommand.SEE_PERMISSION)) {
player.sendMessage(getLang().getPrefixedString("messages.player-not-found", Map.of("player", args[0]))); sender.sendMessage(getLang().getPrefixedString("messages.player-not-found", Map.of("player", args[0])));
return true; return true;
} }
OfflinePlayer target = Bukkit.getOfflinePlayer(args[0]); OfflinePlayer target = Bukkit.getOfflinePlayer(args[0]);
if (!target.hasPlayedBefore() && !target.isOnline()) { if (!target.hasPlayedBefore() && !target.isOnline()) {
player.sendMessage(getLang().getPrefixedString("messages.player-not-found", Map.of("player", args[0]))); sender.sendMessage(getLang().getPrefixedString("messages.player-not-found", Map.of("player", args[0])));
return true; return true;
} }
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
player.sendMessage(getLang().getPrefixedComponent("messages.seen-header", sender.sendMessage(getLang().getPrefixedComponent("messages.seen-header",
Map.of("player", String.valueOf(target.getName())))); Map.of("player", String.valueOf(target.getName()))));
if (target.isOnline()) { if (target.isOnline()) {
player.sendMessage(getLang().getComponent("messages.seen-status-online")); sender.sendMessage(getLang().getComponent("messages.seen-status-online"));
Player onlinePlayer = target.getPlayer(); Player onlinePlayer = target.getPlayer();
if (onlinePlayer != null) { if (onlinePlayer != null) {
player.sendMessage(getLang().getComponent("messages.seen-world", sender.sendMessage(getLang().getComponent("messages.seen-world",
Map.of("world", onlinePlayer.getWorld().getName()))); Map.of("world", onlinePlayer.getWorld().getName())));
} }
} else { } else {
player.sendMessage(getLang().getComponent("messages.seen-status-offline")); sender.sendMessage(getLang().getComponent("messages.seen-status-offline"));
long lastSeen = target.getLastSeen(); long lastSeen = target.getLastSeen();
if (lastSeen > 0) { if (lastSeen > 0) {
player.sendMessage(getLang().getComponent("messages.seen-last-online", sender.sendMessage(getLang().getComponent("messages.seen-last-online",
Map.of("time", format.format(new Date(lastSeen))))); Map.of("time", format.format(new Date(lastSeen)))));
} }
} }
player.sendMessage(getLang().getComponent("messages.seen-first-joined", sender.sendMessage(getLang().getComponent("messages.seen-first-joined",
Map.of("time", format.format(new Date(target.getFirstPlayed()))))); Map.of("time", format.format(new Date(target.getFirstPlayed())))));
return true; return true;
} }
@@ -63,17 +63,37 @@ public class TpsBarCommand extends BaseCommand implements TabCompleter {
@Override @Override
protected boolean executeConsole(CommandSender sender, String[] args) { protected boolean executeConsole(CommandSender sender, String[] args) {
sender.sendMessage(getLang().getPrefixedString("messages.player-only")); TpsBarService tpsBarService = plugin.getTpsBarManager();
if (tpsBarService == null) {
sender.sendMessage(getLang().getPrefixedString("messages.module-disabled"));
return true;
}
if (args.length != 1) {
sender.sendMessage(tpsBarService.getUsageMessage());
return true;
}
if (!sender.hasPermission("essentialsc.command.tpsbar.others")) {
sender.sendMessage(getLang().getPrefixedString("messages.no-permission",
Map.of("permission", "essentialsc.command.tpsbar.others")));
return true;
}
Player target = Bukkit.getPlayerExact(args[0]);
if (target == null) {
sender.sendMessage(tpsBarService.getPlayerNotFoundMessage(args[0]));
return true;
}
boolean enabled = tpsBarService.toggle(target);
sender.sendMessage(getLang().getPrefixedString(enabled
? "tpsbar.messages.enabled-other"
: "tpsbar.messages.disabled-other", Map.of("player", target.getName())));
return true; return true;
} }
@Override @Override
public List<String> onTabComplete(CommandSender sender, Command command, String label, String[] args) { public List<String> onTabComplete(CommandSender sender, Command command, String label, String[] args) {
if (args.length != 1 || !(sender instanceof Player player)) { if (args.length != 1 || !sender.hasPermission("essentialsc.command.tpsbar.others")) {
return List.of();
}
if (!player.hasPermission("essentialsc.command.tpsbar.others")) {
return List.of(); return List.of();
} }
@@ -109,7 +109,7 @@ public class ShulkerBoxListener implements Listener {
} }
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST)
public void onPlayerInteract(PlayerInteractEvent event) { public void onPlayerInteract(PlayerInteractEvent event) {
if (event.getAction() != Action.RIGHT_CLICK_AIR && event.getAction() != Action.RIGHT_CLICK_BLOCK) { if (event.getAction() != Action.RIGHT_CLICK_AIR && event.getAction() != Action.RIGHT_CLICK_BLOCK) {
return; return;
@@ -120,6 +120,10 @@ public class ShulkerBoxListener implements Listener {
return; return;
} }
if (event.useItemInHand() == org.bukkit.event.Event.Result.DENY) {
return;
}
if (openShulkerBoxes.containsKey(player.getUniqueId())) { if (openShulkerBoxes.containsKey(player.getUniqueId())) {
return; return;
} }
@@ -45,7 +45,9 @@ public final class MaintenanceListener implements Listener {
} }
MaintenanceManager.AccessSnapshot snapshot = maintenanceManager.getAccessSnapshot(); MaintenanceManager.AccessSnapshot snapshot = maintenanceManager.getAccessSnapshot();
if (!snapshot.enabled() || snapshot.isWhitelisted(event.getUniqueId(), event.getName())) { if (!snapshot.enabled()
|| snapshot.isWhitelisted(event.getUniqueId(), event.getName())
|| maintenanceManager.isOperator(event.getUniqueId())) {
return; return;
} }
@@ -12,6 +12,7 @@ import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitTask;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -30,13 +31,18 @@ public final class MaintenanceManager {
private final EssentialsC plugin; private final EssentialsC plugin;
private final File configFile; private final File configFile;
private final BukkitTask operatorCacheTask;
private FileConfiguration config; private FileConfiguration config;
private BossBar bossBar; private BossBar bossBar;
private volatile AccessSnapshot accessSnapshot = AccessSnapshot.disabled(); private volatile AccessSnapshot accessSnapshot = AccessSnapshot.disabled();
private volatile Set<UUID> operatorUuids = Set.of();
public MaintenanceManager(EssentialsC plugin) { public MaintenanceManager(EssentialsC plugin) {
this.plugin = plugin; this.plugin = plugin;
this.configFile = new File(plugin.getDataFolder(), "maintenance.yml"); this.configFile = new File(plugin.getDataFolder(), "maintenance.yml");
refreshOperatorCache();
this.operatorCacheTask = plugin.getServer().getScheduler()
.runTaskTimer(plugin, this::refreshOperatorCache, 20L, 20L);
reload(); reload();
} }
@@ -109,7 +115,11 @@ public final class MaintenanceManager {
} }
public boolean canJoin(Player player) { public boolean canJoin(Player player) {
return player != null && (player.hasPermission(getBypassPermission()) || isWhitelisted(player)); return player != null && (player.isOp() || player.hasPermission(getBypassPermission()) || isWhitelisted(player));
}
public boolean isOperator(UUID uniqueId) {
return uniqueId != null && operatorUuids.contains(uniqueId);
} }
public AccessSnapshot getAccessSnapshot() { public AccessSnapshot getAccessSnapshot() {
@@ -231,10 +241,20 @@ public final class MaintenanceManager {
} }
public void shutdown() { public void shutdown() {
operatorCacheTask.cancel();
accessSnapshot = AccessSnapshot.disabled(); accessSnapshot = AccessSnapshot.disabled();
operatorUuids = Set.of();
clearBossBar(); clearBossBar();
} }
private void refreshOperatorCache() {
Set<UUID> operators = new LinkedHashSet<>();
for (org.bukkit.OfflinePlayer operator : plugin.getServer().getOperators()) {
operators.add(operator.getUniqueId());
}
operatorUuids = Set.copyOf(operators);
}
private void clearBossBar() { private void clearBossBar() {
if (bossBar == null) { if (bossBar == null) {
return; return;
+2 -2
View File
@@ -29,11 +29,11 @@ mob-drops:
# 允许末影人掉落搬运的方块。 # 允许末影人掉落搬运的方块。
allow-drops: true allow-drops: true
# TPA 传送请求,行为参考 HuskHomes。 # TPA 传送请求
tpa: tpa:
# 请求有效时间,单位为秒。 # 请求有效时间,单位为秒。
timeout-seconds: 60 timeout-seconds: 60
# 每名接收者最多保留的待处理请求数;参考 EssentialsX超出时移除最早请求。 # 每名接收者最多保留的待处理请求数;超出时移除最早请求。
max-pending-requests: 5 max-pending-requests: 5
# 位置规则 # 位置规则
+10 -5
View File
@@ -1,11 +1,12 @@
# English language file # English language file
# HuskHomes-inspired style: MiniMessage, green success, red errors, gray details. # MiniMessage format with green success, red errors, and gray details.
prefix: '' prefix: '<#00FB9A><bold>[EssentialsC]</bold></#00FB9A><gray>: </gray>'
messages: messages:
no-permission: '<#FF3300>Error:</#FF3300> <#FF7E5E>You do not have permission to use this command.</#FF7E5E> <gray>Required: {permission}</gray>' no-permission: '<#FF3300>Error:</#FF3300> <#FF7E5E>You do not have permission to use this command.</#FF7E5E> <gray>Required: {permission}</gray>'
player-only: '<#FF3300>Error:</#FF3300> <#FF7E5E>This command can only be run by players.</#FF7E5E>' player-only: '<#FF3300>Error:</#FF3300> <#FF7E5E>This command can only be run by players.</#FF7E5E>'
console-name: 'Console'
config-reloaded: '<#00FB9A>Configuration reloaded.</#00FB9A>' config-reloaded: '<#00FB9A>Configuration reloaded.</#00FB9A>'
version: '<#00FB9A>EssentialsC v{version}</#00FB9A>' version: '<#00FB9A>EssentialsC v{version}</#00FB9A>'
paper-version: '<gray>Running on Paper {version}</gray>' paper-version: '<gray>Running on Paper {version}</gray>'
@@ -49,9 +50,11 @@ messages:
heal-self: '<#00FB9A>Your health and hunger have been restored.</#00FB9A>' heal-self: '<#00FB9A>Your health and hunger have been restored.</#00FB9A>'
heal-other: '<#00FB9A>You healed <bold>{player}</bold>.</#00FB9A>' heal-other: '<#00FB9A>You healed <bold>{player}</bold>.</#00FB9A>'
heal-by-other: '<#00FB9A>You were healed by <bold>{admin}</bold>.</#00FB9A>' heal-by-other: '<#00FB9A>You were healed by <bold>{admin}</bold>.</#00FB9A>'
heal-usage-console: '<#FF3300>Error:</#FF3300> <#FF7E5E>Usage: /heal [player]</#FF7E5E>'
feed-self: '<#00FB9A>Your hunger has been restored.</#00FB9A>' feed-self: '<#00FB9A>Your hunger has been restored.</#00FB9A>'
feed-other: '<#00FB9A>You fed <bold>{player}</bold>.</#00FB9A>' feed-other: '<#00FB9A>You fed <bold>{player}</bold>.</#00FB9A>'
feed-by-other: '<#00FB9A>You were fed by <bold>{admin}</bold>.</#00FB9A>' feed-by-other: '<#00FB9A>You were fed by <bold>{admin}</bold>.</#00FB9A>'
feed-usage-console: '<#FF3300>Error:</#FF3300> <#FF7E5E>Usage: /feed [player]</#FF7E5E>'
repair-hand-success: '<#00FB9A>Repaired the item in your hand.</#00FB9A>' repair-hand-success: '<#00FB9A>Repaired the item in your hand.</#00FB9A>'
repair-all-success: '<#00FB9A>Repaired <bold>{count}</bold> item(s).</#00FB9A>' repair-all-success: '<#00FB9A>Repaired <bold>{count}</bold> item(s).</#00FB9A>'
repair-not-damaged: '<#FF3300>Error:</#FF3300> <#FF7E5E>This item is not damaged.</#FF7E5E>' repair-not-damaged: '<#FF3300>Error:</#FF3300> <#FF7E5E>This item is not damaged.</#FF7E5E>'
@@ -76,6 +79,8 @@ help:
footer: '<gray>Each command requires the corresponding permission.</gray>' footer: '<gray>Each command requires the corresponding permission.</gray>'
commands: commands:
reload: '<white>/essc reload</white> <gray>- Reload plugin configuration</gray>'
version: '<white>/essc version</white> <gray>- View plugin and server versions</gray>'
workbench: '<white>/workbench</white> <gray>- Open a crafting table</gray>' workbench: '<white>/workbench</white> <gray>- Open a crafting table</gray>'
anvil: '<white>/anvil</white> <gray>- Open an anvil</gray>' anvil: '<white>/anvil</white> <gray>- Open an anvil</gray>'
enchantingtable: '<white>/enchantingtable</white> <gray>- Open an enchanting table</gray>' enchantingtable: '<white>/enchantingtable</white> <gray>- Open an enchanting table</gray>'
@@ -91,10 +96,10 @@ help:
fly: '<white>/fly</white> <gray>- Toggle flight mode</gray>' fly: '<white>/fly</white> <gray>- Toggle flight mode</gray>'
nightvision: '<white>/nightvision</white> <gray>- Toggle night vision</gray>' nightvision: '<white>/nightvision</white> <gray>- Toggle night vision</gray>'
glow: '<white>/glow</white> <gray>- Toggle glowing</gray>' glow: '<white>/glow</white> <gray>- Toggle glowing</gray>'
heal: '<white>/heal</white> <gray>- Restore health and hunger</gray>' heal: '<white>/heal [player]</white> <gray>- Restore health and hunger</gray>'
vanish: '<white>/vanish</white> <gray>- Toggle vanish mode</gray>' vanish: '<white>/vanish</white> <gray>- Toggle vanish mode</gray>'
seen: '<white>/seen</white> <gray>- View player information</gray>' seen: '<white>/seen [player]</white> <gray>- View player information</gray>'
feed: '<white>/feed</white> <gray>- Restore hunger</gray>' feed: '<white>/feed [player]</white> <gray>- Restore hunger</gray>'
repair: '<white>/repair</white> <gray>- Repair held or all items</gray>' repair: '<white>/repair</white> <gray>- Repair held or all items</gray>'
tpa: '<white>/tpa [player]</white> <gray>- Request to teleport to a player</gray>' tpa: '<white>/tpa [player]</white> <gray>- Request to teleport to a player</gray>'
tpahere: '<white>/tpahere [player]</white> <gray>- Request a player teleport to you</gray>' tpahere: '<white>/tpahere [player]</white> <gray>- Request a player teleport to you</gray>'
+10 -5
View File
@@ -1,11 +1,12 @@
# 简体中文语言文件 # 简体中文语言文件
# 文本风格参考 HuskHomesMiniMessage绿色成功、红色错误灰色说明。 # 支持 MiniMessage 格式,默认使用绿色成功、红色错误灰色说明。
prefix: '' prefix: '<#00FB9A><bold>[EssentialsC]</bold></#00FB9A><gray>: </gray>'
messages: messages:
no-permission: '<#FF3300>错误:</#FF3300> <#FF7E5E>你没有权限执行此命令.</#FF7E5E> <gray>需要权限: {permission}</gray>' no-permission: '<#FF3300>错误:</#FF3300> <#FF7E5E>你没有权限执行此命令.</#FF7E5E> <gray>需要权限: {permission}</gray>'
player-only: '<#FF3300>错误:</#FF3300> <#FF7E5E>此命令只能由玩家执行.</#FF7E5E>' player-only: '<#FF3300>错误:</#FF3300> <#FF7E5E>此命令只能由玩家执行.</#FF7E5E>'
console-name: '控制台'
config-reloaded: '<#00FB9A>配置已重载.</#00FB9A>' config-reloaded: '<#00FB9A>配置已重载.</#00FB9A>'
version: '<#00FB9A>EssentialsC v{version}</#00FB9A>' version: '<#00FB9A>EssentialsC v{version}</#00FB9A>'
paper-version: '<gray>当前运行于 Paper {version}</gray>' paper-version: '<gray>当前运行于 Paper {version}</gray>'
@@ -49,9 +50,11 @@ messages:
heal-self: '<#00FB9A>你的生命值和饥饿值已恢复.</#00FB9A>' heal-self: '<#00FB9A>你的生命值和饥饿值已恢复.</#00FB9A>'
heal-other: '<#00FB9A>你已治疗玩家 <bold>{player}</bold>.</#00FB9A>' heal-other: '<#00FB9A>你已治疗玩家 <bold>{player}</bold>.</#00FB9A>'
heal-by-other: '<#00FB9A>管理员 <bold>{admin}</bold> 治疗了你.</#00FB9A>' heal-by-other: '<#00FB9A>管理员 <bold>{admin}</bold> 治疗了你.</#00FB9A>'
heal-usage-console: '<#FF3300>错误:</#FF3300> <#FF7E5E>用法: /heal [玩家]</#FF7E5E>'
feed-self: '<#00FB9A>你的饥饿值已恢复.</#00FB9A>' feed-self: '<#00FB9A>你的饥饿值已恢复.</#00FB9A>'
feed-other: '<#00FB9A>你已喂饱玩家 <bold>{player}</bold>.</#00FB9A>' feed-other: '<#00FB9A>你已喂饱玩家 <bold>{player}</bold>.</#00FB9A>'
feed-by-other: '<#00FB9A>管理员 <bold>{admin}</bold> 喂饱了你.</#00FB9A>' feed-by-other: '<#00FB9A>管理员 <bold>{admin}</bold> 喂饱了你.</#00FB9A>'
feed-usage-console: '<#FF3300>错误:</#FF3300> <#FF7E5E>用法: /feed [玩家]</#FF7E5E>'
repair-hand-success: '<#00FB9A>手中物品已修复.</#00FB9A>' repair-hand-success: '<#00FB9A>手中物品已修复.</#00FB9A>'
repair-all-success: '<#00FB9A>已修复 <bold>{count}</bold> 个物品.</#00FB9A>' repair-all-success: '<#00FB9A>已修复 <bold>{count}</bold> 个物品.</#00FB9A>'
repair-not-damaged: '<#FF3300>错误:</#FF3300> <#FF7E5E>该物品没有损坏.</#FF7E5E>' repair-not-damaged: '<#FF3300>错误:</#FF3300> <#FF7E5E>该物品没有损坏.</#FF7E5E>'
@@ -76,6 +79,8 @@ help:
footer: '<gray>每个命令都需要对应权限.</gray>' footer: '<gray>每个命令都需要对应权限.</gray>'
commands: commands:
reload: '<white>/essc reload</white> <gray>- 重载插件配置</gray>'
version: '<white>/essc version</white> <gray>- 查看插件与服务端版本</gray>'
workbench: '<white>/workbench</white> <gray>- 打开工作台</gray>' workbench: '<white>/workbench</white> <gray>- 打开工作台</gray>'
anvil: '<white>/anvil</white> <gray>- 打开铁砧</gray>' anvil: '<white>/anvil</white> <gray>- 打开铁砧</gray>'
enchantingtable: '<white>/enchantingtable</white> <gray>- 打开附魔台</gray>' enchantingtable: '<white>/enchantingtable</white> <gray>- 打开附魔台</gray>'
@@ -91,10 +96,10 @@ help:
fly: '<white>/fly</white> <gray>- 切换飞行模式</gray>' fly: '<white>/fly</white> <gray>- 切换飞行模式</gray>'
nightvision: '<white>/nightvision</white> <gray>- 切换夜视</gray>' nightvision: '<white>/nightvision</white> <gray>- 切换夜视</gray>'
glow: '<white>/glow</white> <gray>- 切换发光</gray>' glow: '<white>/glow</white> <gray>- 切换发光</gray>'
heal: '<white>/heal</white> <gray>- 恢复生命值和饥饿值</gray>' heal: '<white>/heal [玩家]</white> <gray>- 恢复生命值和饥饿值</gray>'
vanish: '<white>/vanish</white> <gray>- 切换隐身模式</gray>' vanish: '<white>/vanish</white> <gray>- 切换隐身模式</gray>'
seen: '<white>/seen</white> <gray>- 查看玩家信息</gray>' seen: '<white>/seen [玩家]</white> <gray>- 查看玩家信息</gray>'
feed: '<white>/feed</white> <gray>- 恢复饥饿值</gray>' feed: '<white>/feed [玩家]</white> <gray>- 恢复饥饿值</gray>'
repair: '<white>/repair</white> <gray>- 修复手中或全部物品</gray>' repair: '<white>/repair</white> <gray>- 修复手中或全部物品</gray>'
tpa: '<white>/tpa [玩家]</white> <gray>- 请求传送到玩家身边</gray>' tpa: '<white>/tpa [玩家]</white> <gray>- 请求传送到玩家身边</gray>'
tpahere: '<white>/tpahere [玩家]</white> <gray>- 请求玩家传送到你身边</gray>' tpahere: '<white>/tpahere [玩家]</white> <gray>- 请求玩家传送到你身边</gray>'
@@ -4,6 +4,8 @@ import org.bukkit.configuration.file.YamlConfiguration;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.ValueSource;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
@@ -40,6 +42,9 @@ class ConfigurationResourcesTest {
if (resourcePath.equals("paper-plugin.yml")) { if (resourcePath.equals("paper-plugin.yml")) {
assertEquals("1.21.11", configuration.getString("api-version")); assertEquals("1.21.11", configuration.getString("api-version"));
} }
if (resourcePath.startsWith("lang/")) {
assertTrue(!configuration.getString("prefix", "").isBlank());
}
} }
} }
@@ -51,6 +56,25 @@ class ConfigurationResourcesTest {
assertEquals(chinese.getKeys(true), english.getKeys(true)); assertEquals(chinese.getKeys(true), english.getKeys(true));
} }
@Test
void miniMessageTagNamesAreNormalized() {
assertEquals("<gray>文本</gray>", LangManager.normalizeMiniMessageTags("<GRAY>文本</GRAY>"));
assertEquals("\\<GRAY>", LangManager.normalizeMiniMessageTags("\\<GRAY>"));
}
@Test
void legacyFormattingCanBeNestedInMiniMessage() {
String message = LangManager.applyPlaceholders(
"<gray>维护状态: {status}</gray>",
java.util.Map.of("status", "§a开启")
);
String rendered = PlainTextComponentSerializer.plainText().serialize(
MiniMessage.miniMessage().deserialize(message)
);
assertEquals("维护状态: 开启", rendered);
}
private YamlConfiguration loadResource(String resourcePath) throws Exception { private YamlConfiguration loadResource(String resourcePath) throws Exception {
try (InputStream input = getClass().getClassLoader().getResourceAsStream(resourcePath)) { try (InputStream input = getClass().getClassLoader().getResourceAsStream(resourcePath)) {
assertNotNull(input, resourcePath); assertNotNull(input, resourcePath);