第36批:垃圾站播报改周期内轮次(不再显示2000+)+血条加上轮清扫/倒垃圾数+公告板开关与翻页(开关状态/翻页时间)+公告键兼容中文消息颜色+color继承最近一条+修复顶层标量配置致解析全空
Check Source / check (push) Successful in 4s
Auto Release / build (push) Successful in 3m25s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 2s

This commit is contained in:
youpaishidifu
2026-10-21 22:46:50 +08:00
parent 04678b29b0
commit 2a1ce02e71
3 changed files with 276 additions and 64 deletions
+208 -46
View File
@@ -20,6 +20,7 @@ import org.bukkit.scoreboard.Objective;
import org.bukkit.scoreboard.Score;
import org.bukkit.scoreboard.Scoreboard;
import org.bukkit.scoreboard.ScoreboardManager;
import org.bukkit.scheduler.BukkitTask;
import java.io.File;
import java.io.InputStream;
@@ -107,6 +108,21 @@ public class AnnouncementBoard implements Listener {
private static final Pattern URL_PATTERN =
Pattern.compile("^(https?://\\S+)$", Pattern.CASE_INSENSITIVE);
/** ★ 第36批:公告板总开关(公告.md 顶层键 开关状态:true/1=开,false/0=关) */
private boolean enabled = true;
/** ★ 第36批:翻页秒数(公告.md 顶层键 翻页时间:纯数字=秒) */
private int pageSeconds = 15;
/** ★ 第36批:当前页码(0 起,全局统一页切) */
private int currentPage = 0;
/** ★ 第36批:翻页定时任务 */
private BukkitTask pagerTask = null;
/** ★ 第36批:翻页任务当前周期(秒),与配置不一致才重建 */
private int pagerSeconds = -1;
/** 解析出的公告条目(排序位 → 条目),按排序位升序 */
private final Map<Integer, Notice> notices = new LinkedHashMap<>();
@@ -129,6 +145,17 @@ public class AnnouncementBoard implements Listener {
}
}
/** ★ 第36批:渲染用的一行(公告 + 该行文本),用于按页切片 */
private static final class Row {
final Notice n;
final String line;
Row(Notice n, String line) {
this.n = n;
this.line = line;
}
}
public AnnouncementBoard(Main plugin) {
this.plugin = plugin;
}
@@ -173,6 +200,10 @@ public class AnnouncementBoard implements Listener {
/** 重新解析公告.md */
public void reload() {
// ★ 第36批:配置默认值每次重载重置(md 里没写对应键就回默认)
enabled = true;
pageSeconds = 15;
currentPage = 0;
List<Notice> list = parse();
notices.clear();
for (int i = 0; i < list.size(); i++) {
@@ -182,7 +213,14 @@ public class AnnouncementBoard implements Listener {
if (mdFile != null && mdFile.exists()) {
lastModified = mdFile.lastModified();
}
plugin.getLogger().info("[公告板] 已加载 " + notices.size() + " 条公告");
int rows = totalRows();
int pages = (rows + MAX_SIDEBAR_ROWS - 1) / MAX_SIDEBAR_ROWS;
plugin.getLogger().info("[公告板] 已加载 " + notices.size()
+ " 条公告,开关=" + (enabled ? "开" : "关")
+ ",翻页=" + pageSeconds + "秒,共 " + rows
+ " 行 / " + pages + " 页");
// ★ 第36批:按当前翻页秒数启动/重建翻页任务
restartPager();
}
/** 重载并立即刷新所有玩家 */
@@ -246,26 +284,35 @@ public class AnnouncementBoard implements Listener {
reader.beginObject();
while (reader.hasNext()) {
String key = reader.nextName();
// ★ 第36批:顶层配置键(开关状态/翻页时间)值是标量,
// 必须先识别 —— 否则对 true / 15 调 beginObject() 会抛
// IllegalStateException,整份解析失败、公告板全空。
if (isConfigKey(key)) {
readConfig(key, reader);
continue;
}
if (reader.peek() != JsonToken.BEGIN_OBJECT) {
// 非对象值(标量/数组/NULL)→ 不是公告条目,跳过
reader.skipValue();
continue;
}
String msg = null;
String colorRaw = null;
if (reader.peek() == JsonToken.NULL) {
reader.skipValue();
} else {
reader.beginObject();
while (reader.hasNext()) {
String vk = reader.nextName();
if ("msg".equals(vk)) {
if (reader.peek() == JsonToken.NULL) reader.skipValue();
else msg = reader.nextString();
} else if ("color".equals(vk)) {
if (reader.peek() == JsonToken.NULL) reader.skipValue();
else colorRaw = reader.nextString();
} else {
reader.skipValue();
}
reader.beginObject();
while (reader.hasNext()) {
String vk = reader.nextName();
// ★ 第36批:键名兼容中文(默认 msg/color,兼容 消息/颜色)
if ("msg".equals(vk) || "消息".equals(vk)) {
if (reader.peek() == JsonToken.NULL) reader.skipValue();
else msg = reader.nextString();
} else if ("color".equals(vk) || "颜色".equals(vk)) {
if (reader.peek() == JsonToken.NULL) reader.skipValue();
else colorRaw = reader.nextString();
} else {
reader.skipValue();
}
reader.endObject();
}
reader.endObject();
if (msg == null) continue;
msg = msg.trim();
@@ -293,26 +340,128 @@ public class AnnouncementBoard implements Listener {
return out;
} finally {
if (reader != null) {
try { reader.close(); } catch (Exception ignored) { }
try {
reader.close();
} catch (Exception ignored) {
}
}
}
// ---- 排序(手动选择排序,不用 Comparator/lambda)----
sortBySlot(raw);
// ---- 排序位冲突 → 自动进位(往后找第一个空位)----
Set<Integer> used = new LinkedHashSet<>();
// ★ 第36批:color 继承(用户明令)—— 未填/非法 → 继承最近一条
// 合法 color;一条都没找到 → null(渲染回退 MC 原版白色)
TextColor inherit = null;
for (int i = 0; i < raw.size(); i++) {
Notice n = raw.get(i);
int s2 = n.slot;
if (s2 < 0) s2 = 0;
while (used.contains(s2)) s2++;
used.add(s2);
out.add(new Notice(s2, n.msg, n.color, n.url));
TextColor c = n.color;
if (c == null) c = inherit;
else inherit = c;
out.add(new Notice(s2, n.msg, c, n.url));
}
return out;
}
// ==================== 第36批:配置 / 翻页 ====================
/** ★ 第36批:顶层配置键识别(公告板开关 / 翻页秒数,中文键) */
private static boolean isConfigKey(String key) {
if (key == null) return false;
String k = key.trim();
return "开关状态".equals(k) || "开关".equals(k)
|| "翻页时间".equals(k) || "翻页".equals(k);
}
/**
* ★ 第36批:读标量配置值。
* JSON 的 true/false(BOOLEAN)、15(NUMBER)、"15"(STRING)
* 统一转成 String,交给 parseOnOff / parseInt 处理。
*/
private static String readScalar(JsonReader r) throws java.io.IOException {
JsonToken t = r.peek();
if (t == JsonToken.NULL) {
r.nextNull();
return null;
}
if (t == JsonToken.BOOLEAN) return String.valueOf(r.nextBoolean());
if (t == JsonToken.NUMBER || t == JsonToken.STRING) {
return r.nextString();
}
r.skipValue();
return null;
}
/** ★ 第36批:读取并应用一条顶层配置 */
private void readConfig(String key, JsonReader reader) {
String k = key.trim();
String v;
try {
v = readScalar(reader);
} catch (Exception ex) {
try { reader.skipValue(); } catch (Exception ignored) { }
return;
}
if ("开关状态".equals(k) || "开关".equals(k)) {
enabled = parseOnOff(v);
} else {
// 翻页时间:纯数字=秒(非法/<=0 → 保留默认15)
int sec = -1;
if (v != null) {
try { sec = Integer.parseInt(v.trim()); }
catch (Exception ignored) { }
}
if (sec > 0) pageSeconds = sec;
}
}
/**
* ★ 第36批:开关值解析(用户明令)——
* true / 1 → 开;false / 0 → 关;未识别 → 开(配置写错不至于把板关死)。
*/
static boolean parseOnOff(String v) {
if (v == null) return true;
String t = v.trim().toLowerCase(Locale.ROOT);
if (t.equals("0") || t.equals("false") || t.equals("off")
|| t.equals("no") || t.equals("关") || t.equals("否")) {
return false;
}
return true;
}
/** ★ 第36批:当前公告拆行后的总行数 */
private int totalRows() {
int rows = 0;
for (Notice n : notices.values()) {
rows += splitLine(n.msg).size();
}
return rows;
}
/**
* ★ 第36批:翻页任务 —— 总行数超过侧边栏 15 行时,
* 每「翻页时间」秒切下一页并刷新全员;只有一页时不动作。
*/
private void restartPager() {
int sec = Math.max(1, pageSeconds);
if (pagerTask != null && sec == pagerSeconds) return;
if (pagerTask != null) {
try { pagerTask.cancel(); } catch (Throwable ignored) { }
pagerTask = null;
}
pagerSeconds = sec;
pagerTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
if (!enabled || notices.isEmpty()) return;
int rows = totalRows();
int pages = (rows + MAX_SIDEBAR_ROWS - 1) / MAX_SIDEBAR_ROWS;
if (pages <= 1) return; // 只有一页 → 无需翻页
currentPage = (currentPage + 1) % pages;
applyAll();
}, 20L * sec, 20L * sec);
}
/** 按排序位升序(稳定,手动冒泡,规避 JDK26 javac lambda 类型推断崩溃) */
private static void sortBySlot(List<Notice> list) {
for (int i = 0; i < list.size(); i++) {
@@ -377,7 +526,6 @@ public class AnnouncementBoard implements Listener {
}
return null;
}
// ==================== 渲染 ====================
/**
@@ -401,6 +549,33 @@ public class AnnouncementBoard implements Listener {
try {
ScoreboardManager mgr = Bukkit.getScoreboardManager();
if (mgr == null) return;
// ★ 第36批:开关关闭(开关状态=false/0)→ 归还原版主计分板
if (!enabled) {
p.setScoreboard(mgr.getMainScoreboard());
return;
}
// ★ 第36批:先把公告拆成「行」,再按当前页切片 ——
// 原版侧边栏最多 15 行,超过 15 行自动翻页(翻页时间 秒/页)
List<Row> rows = new ArrayList<>();
for (Notice n : notices.values()) {
List<String> lines = splitLine(n.msg);
for (int li = 0; li < lines.size(); li++) {
rows.add(new Row(n, lines.get(li)));
}
}
if (rows.isEmpty()) {
// 没有可显示的公告 → 归还原版主计分板
p.setScoreboard(mgr.getMainScoreboard());
return;
}
int pages = (rows.size() + MAX_SIDEBAR_ROWS - 1)
/ MAX_SIDEBAR_ROWS;
int page = currentPage;
if (page < 0) page = 0;
if (page >= pages) page = pages - 1;
int from = page * MAX_SIDEBAR_ROWS;
int to = Math.min(from + MAX_SIDEBAR_ROWS, rows.size());
Scoreboard sb = mgr.getNewScoreboard();
Objective obj = sb.registerNewObjective(
OBJECTIVE, Criteria.DUMMY, legacy(TITLE));
@@ -410,32 +585,19 @@ public class AnnouncementBoard implements Listener {
obj.numberFormat(NumberFormat.blank());
} catch (Throwable ignored) {
}
int row = 0; // 已渲染行数
int overflow = 0; // 超出 15 行被丢弃的行数
for (Notice n : notices.values()) {
List<String> lines = splitLine(n.msg);
for (int li = 0; li < lines.size(); li++) {
if (row >= MAX_SIDEBAR_ROWS) {
overflow++;
continue;
}
// entry 用纯 ASCII 唯一串(避免同名条目塌缩)
String entry = "n" + row;
Score sc = obj.getScore(entry);
// 侧边栏默认 descending(高分在上)→ 分数递减,排序位小的排最上
sc.setScore(MAX_SIDEBAR_ROWS - row);
try {
sc.customName(buildComponent(n, lines.get(li)));
} catch (Throwable ignored) {
}
row++;
int row = 0; // 本页已渲染行数
for (int i = from; i < to; i++) {
Row r = rows.get(i);
// entry 用纯 ASCII 唯一串(避免同名条目塌缩)
String entry = "n" + row;
Score sc = obj.getScore(entry);
// 侧边栏默认 descending(高分在上)→ 分数递减,本页首行排最上
sc.setScore(MAX_SIDEBAR_ROWS - row);
try {
sc.customName(buildComponent(r.n, r.line));
} catch (Throwable ignored) {
}
}
if (overflow > 0) {
plugin.getLogger().warning("[公告板] 原版侧边栏最多显示 "
+ MAX_SIDEBAR_ROWS + " 行,已丢弃 "
+ overflow + " 行公告(请精简公告.md)");
row++;
}
p.setScoreboard(sb);
} catch (Exception e) {
+38 -8
View File
@@ -33,6 +33,19 @@ public class GarbageManager implements Listener {
private int maxRounds = 1;
private boolean enabled = true;
private int currentRound = 0;
/**
* ★ 第36批:上一轮「清扫」进回收站的掉落物数量(血条展示)。
* -1 = 还没跑过任何一轮(血条不显示该字段)。
*/
private int lastCleaned = -1;
/**
* ★ 第36批:上一轮「永久倒掉」的过期物品数量(血条展示)。
* -1 = 还没跑过任何一轮;非倒垃圾轮固定为 0。
*/
private int lastDumped = -1;
private final Map<String, Integer>
recyclePages = new HashMap<>();
@@ -288,6 +301,12 @@ public class GarbageManager implements Listener {
}
String title = "§6§l垃圾站 §8| " + label
+ " §8| §f剩余 " + fmtRemain(remain);
// ★ 第36批:血条插一个字段 —— 上一轮清扫数 / 上一轮倒垃圾数
// (还没跑过任何一轮时不显示,避免出现"上轮扫-1个")
if (lastCleaned >= 0 && lastDumped >= 0) {
title += " §8| §7上轮扫§e" + lastCleaned
+ "§7个、倒§e" + lastDumped + "§7个";
}
Component comp = LegacyComponentSerializer
.legacySection().deserialize(title);
@@ -505,13 +524,20 @@ public class GarbageManager implements Listener {
+ deleted + " §f个过期物品");
}
} else {
// ★ 第35批:明确告知当前轮次 / 下一轮倒垃圾的轮次 /
// 还剩几轮才倾倒垃圾(用户要求)
final int nextPurge = k + (n - k % n);
broadcast("§7[垃圾站] §f当前是第 §e" + k
+ " §f轮,第 §e" + nextPurge
// ★ 第36批:轮次口径改「周期内轮次」(用户明令)——
// currentRound 是永久累加值(跑久了就是2000+),
// 对用户没意义;用户要的是 1..N 循环轮次:
// r = ((k-1) % n) + 1,倒垃圾永远在本周期第 n 轮。
// N=2 → k=1 报「第1轮,第2轮倒垃圾,还剩1轮」、
// k=3 回到「第1轮」;
// N=3 → k=1 还剩2轮 / k=2 还剩1轮 / k=3 走 purge 分支。
final int roundInCycle =
((k - 1) % n) + 1;
broadcast("§7[垃圾站] §f当前是第 §e"
+ roundInCycle
+ " §f轮,第 §e" + n
+ " §f轮倒垃圾,还剩 §e"
+ (nextPurge - k)
+ (n - roundInCycle)
+ " §f轮倾倒垃圾");
}
// 递增轮次
@@ -528,6 +554,11 @@ public class GarbageManager implements Listener {
// 收集当前掉落物
// (实体移除必须主线程,落库已在内部转异步)
int count = collectItems();
// ★ 第36批:记录上一轮统计(血条展示)——
// 清扫 = 本轮收进回收站的掉落物数;
// 倒垃圾 = 本轮永久删除的过期物品数(非倒垃圾轮为0)
lastCleaned = count;
lastDumped = deleted;
broadcast("§a§l[垃圾站] §f清理完成,"
+ "共清理 §e" + count
+ " §f个掉落物,存入回收站");
@@ -952,7 +983,6 @@ public class GarbageManager implements Listener {
try {
if (db != null && !db.isClosed())
db.close();
} catch (Exception ignored) {
}
} catch (Exception ignored) {}
}
}
+30 -10
View File
@@ -1,15 +1,35 @@
## ----公告区域开始----
```json
{
"开关状态":true,
"翻页时间":15,
"1": {
"msg": "",
"color": ""
"msg": "草原探险服务器欢迎您",
"color": "green"
},
"2": {
"msg": "",
"color": ""
"msg": "服务器官方群:981954292",
"color": "aqua"
},
"3":{
"msg":"支持版本从1.21.11~26.x",
"color":"aqua"
},
"4":{
"msg":"服务器玩家后台地址↓",
"color":"green"
},
"5":{
"msg":"https://caoyuan.ypshidifu.cn/plugin/player.php",
"color":"green"
},
"6":{
"msg":"服务器ip:mc2.ypshidifu.cn",
"color":"aqua"
},
"7":{
"msg":"Java免端口,基岩版30679"
}
}
```
## ----公告区域结束----
@@ -20,11 +40,11 @@
- 空白键值自动跳过
## 公告区参数选项
| 键 | 值 | 必填 | 解释 |
| --- | --- | --- |---|
| 数字编号 | 花括号 | 必填 | 一条公告的开始 |
| msg | 纯文本消息内容 | 必填 | 消息正文 |
| color | 颜色英文名或16进制颜色代码,或rgb | 非必填 | 指定颜色,未填写回退MC原版白色 |
| 键 | 值 | 必填 | 解释 |
| --- | --- | --- |--------------------------------------------|
| 数字编号 | 花括号 | 必填 | 一条公告的开始 |
| msg | 纯文本消息内容 | 必填 | 消息正文 |
| color | 颜色英文名或16进制颜色代码,或rgb | 非必填 | 指定颜色,未填写回退MC原版逻辑(继承最近一个color参数,如果都没有就回退白色) |
## 示例模板