Skip to content

Commit 213147a

Browse files
committed
release: prepare 1.3.1
1 parent e62c08e commit 213147a

17 files changed

Lines changed: 664 additions & 80 deletions

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
# Changelog
22

3+
## 1.3.1 - 2026-06-18
4+
5+
Hotfix and usability release for CI reliability, command help navigation, and game room selection confirmation.
6+
7+
中文更新日志:
8+
9+
- **Java 编译修复**: 修复 `TableEventCoordinator` 对 Caffeine `Cache` 的旧式 `get(key)` 调用,改为 `getIfPresent`,恢复 Java 编译。
10+
- **Gradle 下载稳定性**: 提高 Gradle Wrapper 下载超时时间并启用重试,降低 GitHub Actions 首次拉取 Gradle 发行包时的超时概率。
11+
- **命令帮助排版**: `/mahjong help` 改为分页式帮助列表,加入标题、副标题、页码状态以及上一页/下一页点击导航。
12+
- **棋室选区粒子预览**: 借鉴 Residence 的领地选择可视化思路,使用粒子描出已选择方块或完整长方体边框,方便创建棋室前确认范围。
13+
14+
English Release Notes:
15+
16+
- **Java compilation fix**: Fixed the outdated Caffeine `Cache#get(key)` call in `TableEventCoordinator` by switching to `getIfPresent`, restoring Java compilation.
17+
- **Gradle download reliability**: Increased the Gradle Wrapper network timeout and enabled retries to reduce first-download timeouts in GitHub Actions.
18+
- **Command help layout**: `/mahjong help` now renders as a paged command list with header, subtitle, page status, and clickable previous/next navigation.
19+
- **Game room selection preview**: Added Residence-inspired particle outlines for the selected block or full cuboid, helping admins confirm the room range before creation.
20+
321
## 1.3.0 - 2026-06-17
422

523
Game room system with core restrictions, wand selection tool, room management commands, bossbar/text mode adaptation, and documentation alignment.

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ plugins {
1212
}
1313

1414
group = "top.ellan"
15-
version = "1.3.0"
15+
version = "1.3.1"
1616

1717
val minimumPaperDevBundleVersion = "1.20.1-R0.1-SNAPSHOT"
1818
val paperDevBundleVersion =
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
33
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
4-
networkTimeout=10000
5-
retries=0
6-
retryBackOffMs=500
4+
networkTimeout=120000
5+
retries=3
6+
retryBackOffMs=1000
77
validateDistributionUrl=true
88
zipStoreBase=GRADLE_USER_HOME
99
zipStorePath=wrapper/dists

src/main/java/top/ellan/mahjong/bootstrap/MahjongPaperPlugin.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import top.ellan.mahjong.db.DatabaseService;
1010
import top.ellan.mahjong.gb.jni.GbNativeWarmupService;
1111
import top.ellan.mahjong.gameroom.GameRoomManager;
12+
import top.ellan.mahjong.gameroom.GameRoomSelectionPreviewService;
1213
import top.ellan.mahjong.gameroom.GameRoomSelectionService;
1314
import top.ellan.mahjong.gameroom.GameRoomWandListener;
1415
import top.ellan.mahjong.i18n.MessageService;
@@ -47,6 +48,7 @@ public final class MahjongPaperPlugin extends JavaPlugin {
4748
private MahjongTableManager tableManager;
4849
private GameRoomManager gameRoomManager;
4950
private GameRoomSelectionService gameRoomSelectionService;
51+
private GameRoomSelectionPreviewService gameRoomSelectionPreviewService;
5052
private PluginTask gameRoomTickTask;
5153

5254
@Override
@@ -85,6 +87,7 @@ public void onEnable() {
8587
this.tableManager.loadPersistentTables();
8688

8789
this.gameRoomSelectionService = new GameRoomSelectionService();
90+
this.gameRoomSelectionPreviewService = new GameRoomSelectionPreviewService(this.gameRoomSelectionService, this.scheduler);
8891
this.gameRoomManager = new GameRoomManager(
8992
this.tableManager,
9093
() -> this.debug,
@@ -112,7 +115,7 @@ public void onEnable() {
112115

113116
this.getServer().getPluginManager().registerEvents(this.tableManager, this);
114117
this.getServer().getPluginManager().registerEvents(new top.ellan.mahjong.gameroom.GameRoomListener(() -> this.gameRoomManager, this.messages, () -> this.settings), this);
115-
this.getServer().getPluginManager().registerEvents(new GameRoomWandListener(this.gameRoomSelectionService, this.messages, () -> this.settings), this);
118+
this.getServer().getPluginManager().registerEvents(new GameRoomWandListener(this.gameRoomSelectionService, this.gameRoomSelectionPreviewService, this.messages, () -> this.settings), this);
116119
this.gameRoomTickTask = this.scheduler.runGlobalTimer(() -> {
117120
if (this.gameRoomManager != null) {
118121
this.gameRoomManager.tick();
@@ -138,6 +141,10 @@ public void onDisable() {
138141
this.gameRoomTickTask.cancel();
139142
this.gameRoomTickTask = null;
140143
}
144+
if (this.gameRoomSelectionPreviewService != null) {
145+
this.gameRoomSelectionPreviewService.cancelAll();
146+
this.gameRoomSelectionPreviewService = null;
147+
}
141148
if (this.tableManager != null) {
142149
this.tableManager.shutdown();
143150
}
@@ -372,6 +379,9 @@ public String reloadMahjongConfiguration() {
372379
if (this.gameRoomSelectionService != null && !reloadedSettings.gameRooms().enabled()) {
373380
this.gameRoomSelectionService.clearAll();
374381
}
382+
if (this.gameRoomSelectionPreviewService != null && !reloadedSettings.gameRooms().enabled()) {
383+
this.gameRoomSelectionPreviewService.cancelAll();
384+
}
375385
this.debug.log("lifecycle", "Debug logging enabled.");
376386
this.notifySettingsListeners(previousSettings, reloadedSettings);
377387

@@ -458,5 +468,3 @@ private java.nio.file.Path gameRoomStoragePath(PluginSettings settings) {
458468
return this.getDataFolder().toPath().resolve(file);
459469
}
460470
}
461-
462-

src/main/java/top/ellan/mahjong/command/MahjongCommandContext.java

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
import java.util.function.Supplier;
1212
import kotlin.Pair;
1313
import net.kyori.adventure.text.Component;
14+
import net.kyori.adventure.text.TextComponent;
15+
import net.kyori.adventure.text.event.ClickEvent;
16+
import net.kyori.adventure.text.event.HoverEvent;
17+
import net.kyori.adventure.text.format.NamedTextColor;
1418
import org.bukkit.command.CommandSender;
1519
import org.bukkit.entity.Player;
1620
import top.ellan.mahjong.debug.DebugService;
@@ -37,7 +41,9 @@
3741

3842
public final class MahjongCommandContext {
3943
public static final String ADMIN_PERMISSION = "mahjongpaper.admin";
44+
private static final int HELP_PAGE_SIZE = 10;
4045
static final java.util.List<String> HELP_KEY_ORDER = java.util.List.of(
46+
"command.help.help",
4147
"command.help.create",
4248
"command.help.botmatch",
4349
"command.help.mode",
@@ -147,14 +153,40 @@ public MahjongTableSession requireViewedTable(Player player) {
147153
}
148154

149155
public void sendHelp(Player player) {
156+
this.sendHelp(player, 1);
157+
}
158+
159+
public void sendHelp(Player player, int requestedPage) {
150160
Locale locale = this.messages.resolveLocale(player);
151-
this.messages.send(player, "command.usage");
152-
for (String key : HELP_KEYS) {
153-
if (ADMIN_HELP_KEYS.contains(key) && !player.hasPermission(ADMIN_PERMISSION)) {
154-
continue;
155-
}
156-
player.sendMessage(this.messages.render(locale, key));
161+
List<String> visibleKeys = this.visibleHelpKeys(player);
162+
int pageCount = Math.max(1, (int) Math.ceil((double) visibleKeys.size() / HELP_PAGE_SIZE));
163+
int page = Math.max(1, Math.min(requestedPage, pageCount));
164+
int start = (page - 1) * HELP_PAGE_SIZE;
165+
int end = Math.min(start + HELP_PAGE_SIZE, visibleKeys.size());
166+
167+
player.sendMessage(this.messages.render(locale, "command.help.header"));
168+
player.sendMessage(this.messages.render(locale, "command.help.subtitle"));
169+
player.sendMessage(this.messages.render(
170+
locale,
171+
"command.help.page_status",
172+
this.messages.number(locale, "page", page),
173+
this.messages.number(locale, "pages", pageCount),
174+
this.messages.number(locale, "count", visibleKeys.size())
175+
));
176+
for (int index = start; index < end; index++) {
177+
player.sendMessage(Component.text(" - ", NamedTextColor.DARK_AQUA).append(this.messages.render(locale, visibleKeys.get(index))));
157178
}
179+
player.sendMessage(this.helpNavigation(locale, page, pageCount));
180+
player.sendMessage(this.messages.render(locale, "command.help.footer"));
181+
}
182+
183+
public List<String> suggestedHelpPages(Player player, String rawPrefix) {
184+
int pageCount = this.helpPageCount(player);
185+
List<String> pages = new ArrayList<>(pageCount);
186+
for (int page = 1; page <= pageCount; page++) {
187+
pages.add(String.valueOf(page));
188+
}
189+
return this.matchPrefix(rawPrefix, pages);
158190
}
159191

160192
public void sendReaction(Player player, ReactionType type, Pair<MahjongTile, MahjongTile> pair, String actionKey) {
@@ -439,6 +471,51 @@ private String rankLabel(Locale locale, MahjongSoulRankProfile profile) {
439471
return this.messages.plain(locale, tierKey) + " " + profile.level();
440472
}
441473

474+
private List<String> visibleHelpKeys(Player player) {
475+
List<String> keys = new ArrayList<>();
476+
for (String key : HELP_KEYS) {
477+
if (ADMIN_HELP_KEYS.contains(key) && !player.hasPermission(ADMIN_PERMISSION)) {
478+
continue;
479+
}
480+
keys.add(key);
481+
}
482+
return List.copyOf(keys);
483+
}
484+
485+
private int helpPageCount(Player player) {
486+
return Math.max(1, (int) Math.ceil((double) this.visibleHelpKeys(player).size() / HELP_PAGE_SIZE));
487+
}
488+
489+
private Component helpNavigation(Locale locale, int page, int pageCount) {
490+
TextComponent.Builder builder = Component.text();
491+
builder.append(Component.text(" "));
492+
builder.append(this.helpPageButton(locale, "command.help.previous", page - 1, page > 1));
493+
builder.append(Component.text(" "));
494+
builder.append(this.messages.render(
495+
locale,
496+
"command.help.page_compact",
497+
this.messages.number(locale, "page", page),
498+
this.messages.number(locale, "pages", pageCount)
499+
));
500+
builder.append(Component.text(" "));
501+
builder.append(this.helpPageButton(locale, "command.help.next", page + 1, page < pageCount));
502+
return builder.build();
503+
}
504+
505+
private Component helpPageButton(Locale locale, String labelKey, int targetPage, boolean enabled) {
506+
String label = this.messages.plain(locale, labelKey);
507+
Component button = Component.text("[", NamedTextColor.DARK_GRAY)
508+
.append(Component.text(label, enabled ? NamedTextColor.YELLOW : NamedTextColor.DARK_GRAY))
509+
.append(Component.text("]", NamedTextColor.DARK_GRAY));
510+
if (!enabled) {
511+
return button;
512+
}
513+
String command = "/mahjong help " + targetPage;
514+
return button
515+
.clickEvent(ClickEvent.runCommand(command))
516+
.hoverEvent(HoverEvent.showText(Component.text(command, NamedTextColor.GRAY)));
517+
}
518+
442519
public void showLeaderboard(Player player, MahjongVariant requestedMode) {
443520
DatabaseService database = this.database();
444521
if (database == null || !database.rankingEnabled()) {
Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package top.ellan.mahjong.command.subcommand;
22

3+
import java.util.List;
34
import org.bukkit.command.CommandSender;
45
import org.bukkit.entity.Player;
56
import top.ellan.mahjong.command.MahjongCommandContext;
@@ -8,5 +9,23 @@
89
public final class HelpSubcommand extends AbstractMahjongSubcommand {
910
public HelpSubcommand(MahjongCommandContext context) { super(context); }
1011
public MahjongSubcommand create() { return this.subcommand("help"); }
11-
@Override protected void execute(CommandSender sender, Player player, String[] args) { this.context.sendHelp(player); }
12-
}
12+
@Override protected void execute(CommandSender sender, Player player, String[] args) {
13+
if (args.length < 2) {
14+
this.context.sendHelp(player);
15+
return;
16+
}
17+
this.context.sendHelp(player, this.parsePage(args[1]));
18+
}
19+
20+
@Override protected List<String> suggest(Player player, String[] args) {
21+
return args.length == 2 ? this.context.suggestedHelpPages(player, args[1]) : List.of();
22+
}
23+
24+
private int parsePage(String raw) {
25+
try {
26+
return Integer.parseInt(raw);
27+
} catch (NumberFormatException exception) {
28+
return 1;
29+
}
30+
}
31+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package top.ellan.mahjong.gameroom;
2+
3+
import java.util.List;
4+
import java.util.Map;
5+
import java.util.UUID;
6+
import java.util.concurrent.ConcurrentHashMap;
7+
import org.bukkit.Color;
8+
import org.bukkit.Location;
9+
import org.bukkit.Particle;
10+
import org.bukkit.entity.Player;
11+
import top.ellan.mahjong.compat.PaperCompatibility;
12+
import top.ellan.mahjong.runtime.PluginTask;
13+
import top.ellan.mahjong.runtime.ServerScheduler;
14+
15+
public final class GameRoomSelectionPreviewService {
16+
private static final int PREVIEW_PULSES = 8;
17+
private static final long PREVIEW_PERIOD_TICKS = 10L;
18+
private static final Particle.DustOptions OUTLINE_DUST = new Particle.DustOptions(Color.fromRGB(60, 220, 255), 1.15F);
19+
20+
private final GameRoomSelectionService selectionService;
21+
private final ServerScheduler scheduler;
22+
private final Map<UUID, PreviewSession> sessions = new ConcurrentHashMap<>();
23+
24+
public GameRoomSelectionPreviewService(GameRoomSelectionService selectionService, ServerScheduler scheduler) {
25+
this.selectionService = selectionService;
26+
this.scheduler = scheduler;
27+
}
28+
29+
public void showSelection(Player player) {
30+
if (player == null || this.selectionService == null || this.scheduler == null) {
31+
return;
32+
}
33+
this.showSelection(player, this.selectionService.selection(player.getUniqueId()));
34+
}
35+
36+
public void showSelection(Player player, GameRoomSelectionService.Selection selection) {
37+
if (player == null || selection == null || this.scheduler == null) {
38+
return;
39+
}
40+
UUID playerId = player.getUniqueId();
41+
this.cancel(playerId);
42+
43+
List<Location> points = this.selectionService.previewPoints(selection, player.getLocation());
44+
if (points.isEmpty()) {
45+
return;
46+
}
47+
PreviewSession session = new PreviewSession(points, PREVIEW_PULSES);
48+
this.sessions.put(playerId, session);
49+
this.schedule(player, session, 1L);
50+
}
51+
52+
public void cancel(UUID playerId) {
53+
if (playerId == null) {
54+
return;
55+
}
56+
PreviewSession session = this.sessions.remove(playerId);
57+
if (session != null) {
58+
session.cancel();
59+
}
60+
}
61+
62+
public void cancelAll() {
63+
for (UUID playerId : List.copyOf(this.sessions.keySet())) {
64+
this.cancel(playerId);
65+
}
66+
}
67+
68+
private void schedule(Player player, PreviewSession session, long delayTicks) {
69+
session.task = this.scheduler.runEntityDelayed(player, () -> this.pulse(player, session), delayTicks);
70+
}
71+
72+
private void pulse(Player player, PreviewSession session) {
73+
if (player == null || this.sessions.get(player.getUniqueId()) != session) {
74+
return;
75+
}
76+
if (!player.isOnline()) {
77+
this.cancel(player.getUniqueId());
78+
return;
79+
}
80+
for (Location point : session.points()) {
81+
player.spawnParticle(
82+
PaperCompatibility.dustParticle(),
83+
point,
84+
1,
85+
0.0D,
86+
0.0D,
87+
0.0D,
88+
0.0D,
89+
OUTLINE_DUST
90+
);
91+
}
92+
session.remainingPulses--;
93+
if (session.remainingPulses <= 0) {
94+
this.sessions.remove(player.getUniqueId(), session);
95+
return;
96+
}
97+
this.schedule(player, session, PREVIEW_PERIOD_TICKS);
98+
}
99+
100+
private static final class PreviewSession {
101+
private final List<Location> points;
102+
private int remainingPulses;
103+
private PluginTask task;
104+
105+
private PreviewSession(List<Location> points, int remainingPulses) {
106+
this.points = List.copyOf(points);
107+
this.remainingPulses = remainingPulses;
108+
}
109+
110+
private List<Location> points() {
111+
return this.points;
112+
}
113+
114+
private void cancel() {
115+
if (this.task != null && !this.task.isCancelled()) {
116+
this.task.cancel();
117+
}
118+
}
119+
}
120+
}

0 commit comments

Comments
 (0)