跳到主要内容

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

  • Main creates GuiManager during runtime manager initialization.
  • Main also creates SongSelectionManager as the reusable entrypoint for code-driven chart selection flows.
  • Main registers GuiListener during 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 paged ArenaSettingsMenu, and JudgeSoundSettingsMenu.
    • Demonstrates enum cycling, boolean toggles, numeric adjustment, sound customization with live preview, paged selection, stacked parent fallback, and remote option sync fallback.
  • 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 GameSettingsMenu can be opened from inside the selection screen and returned from cleanly.

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 from MessageFields instead of hardcoding raw lore text, and customLorePlaceholders() can fill placeholders like {target}.
  • SongSelectionRequest.allowAutoPlay() lets flows such as /duel lock the autoplay button off while still reusing the same GUI.
  • The random button uses song-select.random.duration-ticks from config.yml and an OUT_QUINT easing curve to roll from right to left and decelerate before stopping.
  • The selection result now carries an autoPlay flag, and the built-in /start flow applies it to gameplay so notes are judged automatically once they reach their hit time.
  • Duel selection keeps autoPlay forced 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 MenuButton handlers.
  • Use MenuRenderContext / MenuClickContext to access the Bukkit Player, nullable RhyPlayer, active menu, and current session.
  • For paged menus, extend PagedChestMenu<T> and implement getEntries(...) plus renderEntry(...).
  • For nested menus, open the next menu from a button handler with click.reopen(new AnotherMenu()).
  • For option editors, update PlayerOptions first, then call click.refresh() or reopen another menu.

Guardrails

  • GUI operations must stay on the Bukkit main thread. GuiManager enforces 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.