GUI Menu Framework
RhythMC-Reborn now includes a reusable chest-based GUI framework for all future menus.
Goals
- Base every menu on Bukkit chest inventories.
- Keep menu construction code-driven and strongly typed.
- Reuse one listener/session pipeline across all GUI screens.
- Support dynamic redraw, pagination, and nested menu transitions.
- Avoid coupling the framework to ACF or any specific command entrypoint.
Core Classes
src/main/java/cn/frkovo/rhythmcv2/Gui/GuiManager.java- Opens, refreshes, and closes menus.
- Tracks one active GUI session per player.
- Maintains a menu history stack for explicit back-navigation between child menus.
- Ensures inventory operations happen on the main thread.
src/main/java/cn/frkovo/rhythmcv2/Gui/GuiListener.java- Handles click, drag, close, and quit events.
- Delegates behavior to
GuiManager.
src/main/java/cn/frkovo/rhythmcv2/Gui/ChestMenu.java- Base abstraction for any chest GUI.
- Menus define title, row count, and
render(...).
src/main/java/cn/frkovo/rhythmcv2/Gui/PagedChestMenu.java- Base abstraction for list-style menus with pagination state.
src/main/java/cn/frkovo/rhythmcv2/Gui/MenuRenderer.java- Writes items/buttons into the inventory without exposing session internals.
src/main/java/cn/frkovo/rhythmcv2/Gui/MenuButton.java- Couples an item supplier with a click handler.
src/main/java/cn/frkovo/rhythmcv2/Gui/GuiSession.java- Stores the active menu, inventory, and slot button map for one player.
Lifecycle Integration
MaincreatesGuiManagerduring runtime manager initialization.Mainalso createsSongSelectionManageras the reusable entrypoint for code-driven chart selection flows.MainregistersGuiListenerduring plugin event registration.Main.onDisable()closes all active GUI sessions before full shutdown completes.
This keeps GUI support globally available without forcing any menu to use a specific command framework.
Usage Pattern
Create a menu by extending ChestMenu:
public final class ExampleMenu extends ChestMenu {
@Override
public String getTitle(Player player, RhyPlayer rhyPlayer) {
return "Example";
}
@Override
public int getRows(Player player, RhyPlayer rhyPlayer) {
return 6;
}
@Override
public void render(MenuRenderer renderer) {
renderer.setButton(13, MenuButton.of(
context -> buildExampleItem(),
click -> click.player().sendMessage("clicked")
));
}
}
Open it from any existing flow:
Main.getGuiManager().open(player, new ExampleMenu());
Refresh the current screen after local state changes:
Main.getGuiManager().refresh(player);
Close it explicitly:
Main.getGuiManager().close(player);
Built-In Menu
src/main/java/cn/frkovo/rhythmcv2/Gui/Menus/SettingsMenu.java- Opened with
/settings. - Uses only i18n-backed button names, lore, and feedback messages.
- Provides a settings hub that branches into
AccountSettingsMenu,GameSettingsMenu, the pagedArenaSettingsMenu, andJudgeSoundSettingsMenu. - Demonstrates enum cycling, boolean toggles, numeric adjustment, sound customization with live preview, paged selection, stacked parent fallback, and remote option sync fallback.
- Opened with
src/main/java/cn/frkovo/rhythmcv2/Gui/Menus/SongSelectionMenu.java- Opened programmatically through
src/main/java/cn/frkovo/rhythmcv2/Gui/SongSelectionManager.java. - Returns a
CompletableFuture<SongSelectionResult>so duel, matchmaking, or later lobby flows can wait on a single async selection result. - Supports custom titles, difficulty switching, curated-collection filtering hooks, preview playback, random easing-based song rolling, and an optional per-selection autoplay flag that callers can disable for restricted flows like duel.
- Reuses the existing child-menu history stack so
GameSettingsMenucan be opened from inside the selection screen and returned from cleanly.
- Opened programmatically through
Reusable Song Selection Flow
Use the manager from any gameplay or command flow:
Main.getSongSelectionManager()
.open(rhyPlayer, new SongSelectionRequest(
"决斗 - Alice",
MessageFields.DUEL_COMMAND_SELECT_LORE,
Map.of("target", "Alice")
))
.thenAccept(result -> {
if (result == null) {
return;
}
int chartId = result.chartId();
boolean autoPlay = result.autoPlay();
});
Behavior notes:
- Clicking a song entry completes the future immediately with the currently highlighted chart.
- Clicking the start button completes the future with the current selection.
- Closing the GUI without choosing cancels the future instead of returning a fake result.
SongSelectionRequest.customLoreKey()lets callers reuse i18n lore lists fromMessageFieldsinstead of hardcoding raw lore text, andcustomLorePlaceholders()can fill placeholders like{target}.SongSelectionRequest.allowAutoPlay()lets flows such as/duellock the autoplay button off while still reusing the same GUI.- The random button uses
song-select.random.duration-ticksfromconfig.ymland anOUT_QUINTeasing curve to roll from right to left and decelerate before stopping. - The selection result now carries an
autoPlayflag, and the built-in/startflow applies it to gameplay so notes are judged automatically once they reach their hit time. - Duel selection keeps
autoPlayforced off, and autoplay gameplay results are intentionally not uploaded to the cloud result API. - The song wheel now keeps the current choice locked to the center lane under a dripstone marker, and short song lists wrap/repeat to keep the wheel visually full.
- The top-right curated collection button now opens a dedicated paged collection GUI with unlock-aware status, song counts, and requirement previews for locked collections.
- The song screen now supports persistent per-player sort modes, including score, added date, hotness, play count, and level.
- Song entries now expose unlock state and resource-pack playability in lore, and locked entries render as barriers instead of disappearing from the wheel.
- Clicking a song icon now only changes the current selection; actual start remains on the dedicated start button.
- Chapter-pack-only unplayable songs now guide the player into the chapter pack selection/download flow instead of trying to start immediately.
- The song GUI now restores each player's last difficulty, collection, highlighted song, autoplay flag, and sort mode after closing and reopening.
Extension Notes
- Put all clickable behavior inside
MenuButtonhandlers. - Use
MenuRenderContext/MenuClickContextto access the BukkitPlayer, nullableRhyPlayer, active menu, and current session. - For paged menus, extend
PagedChestMenu<T>and implementgetEntries(...)plusrenderEntry(...). - For nested menus, open the next menu from a button handler with
click.reopen(new AnotherMenu()). - For option editors, update
PlayerOptionsfirst, then callclick.refresh()or reopen another menu.
Guardrails
- GUI operations must stay on the Bukkit main thread.
GuiManagerenforces this for open/refresh/close paths. - The framework identifies its own inventories via
GuiInventoryHolder; do not route menu logic by title text. - Bottom inventory interaction is cancelled by default. Override
shouldCancelBottomInventoryClick(...)only when a menu intentionally allows player inventory interaction. - The framework is intentionally code-first for now. If YAML-defined menus are needed later, build them on top of this layer rather than bypassing it.