All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.incendo.cloud.minecraft.extras.MinecraftHelp Maven / Gradle / Ivy

//
// MIT License
//
// Copyright (c) 2024 Incendo
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
package org.incendo.cloud.minecraft.extras;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.LinearComponents;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.apiguardian.api.API;
import org.checkerframework.checker.index.qual.NonNegative;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.immutables.value.Value;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.caption.Caption;
import org.incendo.cloud.caption.CaptionProvider;
import org.incendo.cloud.caption.CaptionRegistry;
import org.incendo.cloud.caption.CaptionVariable;
import org.incendo.cloud.component.CommandComponent;
import org.incendo.cloud.description.CommandDescription;
import org.incendo.cloud.description.Description;
import org.incendo.cloud.help.CommandPredicate;
import org.incendo.cloud.help.HelpHandler;
import org.incendo.cloud.help.HelpQuery;
import org.incendo.cloud.help.result.CommandEntry;
import org.incendo.cloud.help.result.HelpQueryResult;
import org.incendo.cloud.help.result.IndexCommandResult;
import org.incendo.cloud.help.result.MultipleCommandResult;
import org.incendo.cloud.help.result.VerboseCommandResult;
import org.incendo.cloud.internal.ImmutableImpl;
import org.incendo.cloud.internal.StagedImmutableBuilder;
import org.incendo.cloud.minecraft.extras.caption.ComponentCaptionFormatter;
import org.incendo.cloud.type.tuple.Pair;
import org.incendo.cloud.util.StringUtils;

import static net.kyori.adventure.text.Component.space;
import static net.kyori.adventure.text.Component.text;
import static net.kyori.adventure.text.event.ClickEvent.runCommand;

/**
 * Opinionated extension of {@link org.incendo.cloud.help.HelpRenderer} for Minecraft.
 * 

* This class should not be extended, and the only implementation of it is {@link ImmutableMinecraftHelp}. *

* You may customize certain aspects of the help menu by using the {@link #builder() builder}. * * @param command sender type */ @StagedImmutableBuilder @Value.Immutable @SuppressWarnings("unused") public abstract class MinecraftHelp { public static final int DEFAULT_HEADER_FOOTER_LENGTH = 46; public static final int DEFAULT_MAX_RESULTS_PER_PAGE = 6; /** * The default color scheme for {@link MinecraftHelp} */ public static final HelpColors DEFAULT_HELP_COLORS = helpColors( NamedTextColor.GOLD, NamedTextColor.GREEN, NamedTextColor.YELLOW, NamedTextColor.GRAY, NamedTextColor.DARK_GRAY ); public static final String MESSAGE_HELP_TITLE = "help"; public static final String MESSAGE_COMMAND = "command"; public static final String MESSAGE_DESCRIPTION = "description"; public static final String MESSAGE_NO_DESCRIPTION = "no_description"; public static final String MESSAGE_ARGUMENTS = "arguments"; public static final String MESSAGE_OPTIONAL = "optional"; public static final String MESSAGE_SHOWING_RESULTS_FOR_QUERY = "showing_results_for_query"; public static final String MESSAGE_NO_RESULTS_FOR_QUERY = "no_results_for_query"; public static final String MESSAGE_AVAILABLE_COMMANDS = "available_commands"; public static final String MESSAGE_CLICK_TO_SHOW_HELP = "click_to_show_help"; public static final String MESSAGE_PAGE_OUT_OF_RANGE = "page_out_of_range"; public static final String MESSAGE_CLICK_FOR_NEXT_PAGE = "click_for_next_page"; public static final String MESSAGE_CLICK_FOR_PREVIOUS_PAGE = "click_for_previous_page"; private static final Pattern STRING_PLACEHOLDER_PATTERN = Pattern.compile("<([a-z_]+)>"); private static final Map DEFAULT_MESSAGES = new HashMap<>(); static { DEFAULT_MESSAGES.put(MESSAGE_HELP_TITLE, "Help"); DEFAULT_MESSAGES.put(MESSAGE_COMMAND, "Command"); DEFAULT_MESSAGES.put(MESSAGE_DESCRIPTION, "Description"); DEFAULT_MESSAGES.put(MESSAGE_NO_DESCRIPTION, "No description"); DEFAULT_MESSAGES.put(MESSAGE_ARGUMENTS, "Arguments"); DEFAULT_MESSAGES.put(MESSAGE_OPTIONAL, "Optional"); DEFAULT_MESSAGES.put(MESSAGE_SHOWING_RESULTS_FOR_QUERY, "Showing search results for query"); DEFAULT_MESSAGES.put(MESSAGE_NO_RESULTS_FOR_QUERY, "No results for query"); DEFAULT_MESSAGES.put(MESSAGE_AVAILABLE_COMMANDS, "Available Commands"); DEFAULT_MESSAGES.put(MESSAGE_CLICK_TO_SHOW_HELP, "Click to show help for this command"); DEFAULT_MESSAGES.put(MESSAGE_PAGE_OUT_OF_RANGE, "Error: Page is not in range. Must be in range [1, ]"); DEFAULT_MESSAGES.put(MESSAGE_CLICK_FOR_NEXT_PAGE, "Click for next page"); DEFAULT_MESSAGES.put(MESSAGE_CLICK_FOR_PREVIOUS_PAGE, "Click for previous page"); } /** * Returns a {@link CaptionProvider} for the default messages. * * @param command sender type * @return default captions provider * @see #captionMessageProvider(CaptionRegistry, ComponentCaptionFormatter) */ public static CaptionProvider defaultCaptionsProvider() { return CaptionProvider.constantProvider() .putAllCaptions( DEFAULT_MESSAGES.entrySet().stream() .map(e -> Pair.of(Caption.of("help.minecraft." + e.getKey()), e.getValue())) .collect(Collectors.toMap(Pair::first, Pair::second)) ) .build(); } /** * Constructs a new Minecraft help instance for a sender type which is an {@link Audience}. * * @param commandPrefix command that was used to trigger the help menu. Used to help insertion generation * @param commandManager command manager * @param sender type extending {@link Audience} * @return new MinecraftHelp instance * @since 1.5.0 */ @API(status = API.Status.STABLE, since = "1.5.0") public static @NonNull MinecraftHelp createNative( final @NonNull String commandPrefix, final @NonNull CommandManager commandManager ) { return ImmutableMinecraftHelp.builder() .commandManager(commandManager) .audienceProvider(AudienceProvider.nativeAudience()) .commandPrefix(commandPrefix) .build(); } /** * Constructs a new Minecraft help instance using the default values. * * @param commandPrefix command that was used to trigger the help menu. Used to help insertion generation * @param commandManager command manager * @param audienceProvider mapper from {@link C} to {@link Audience} * @param sender type extending {@link Audience} * @return new MinecraftHelp instance * @since 2.0.0 */ @API(status = API.Status.STABLE, since = "2.0.0") public static @NonNull MinecraftHelp create( final @NonNull String commandPrefix, final @NonNull CommandManager commandManager, final @NonNull AudienceProvider audienceProvider ) { return ImmutableMinecraftHelp.builder() .commandManager(commandManager) .audienceProvider(audienceProvider) .commandPrefix(commandPrefix) .build(); } /** * Returns a new {@link MinecraftHelp} builder. * * @param the command sender type * @return the builder * @since 2.0.0 */ @API(status = API.Status.STABLE, since = "2.0.0") public static ImmutableMinecraftHelp.@NonNull CommandManagerBuildStage builder() { return ImmutableMinecraftHelp.builder(); } MinecraftHelp() { } /** * Returns the command manager instance. * * @return command manager */ public abstract @NonNull CommandManager commandManager(); /** * Returns the audience provider that was used to create this instance. * * @return audience provider */ public abstract @NonNull AudienceProvider audienceProvider(); /** * Returns the command prefix. * * @return command prefix * @since 2.0.0 */ @API(status = API.Status.STABLE, since = "2.0.0") public abstract @NonNull String commandPrefix(); /** * Returns the help handler. * * @return the help handler * @since 2.0.0 */ @Value.Derived @API(status = API.Status.STABLE, since = "2.0.0") public @NonNull HelpHandler helpHandler() { return this.commandManager().createHelpHandler(this.commandFilter()); } /** * Maps a command sender to an {@link Audience} * * @param sender Sender to map * @return Mapped audience */ public @NonNull Audience audience(final @NonNull C sender) { return this.audienceProvider().apply(sender); } /** * Returns the filter that determines what commands are visible inside the help menu. *

* The default filter is {@link org.incendo.cloud.help.CommandPredicate#acceptAll()}. * * @return the filter * @since 2.0.0 */ @Value.Default @API(status = API.Status.STABLE, since = "2.0.0") public @NonNull CommandPredicate commandFilter() { return CommandPredicate.acceptAll(); } /** * Returns the description decorator which turns descriptions into components. *

* The default decorator is {@link DescriptionDecorator#text()}. * * @return the decorator * @since 2.0.0 */ @Value.Default @API(status = API.Status.STABLE, since = "2.0.0") public @NonNull DescriptionDecorator descriptionDecorator() { return DescriptionDecorator.text(); } /** * Returns the messages which are used by the default {@link MessageProvider}. *

* Placeholders in the format {@literal } will be replaced. * * @return the messages * @since 2.0.0 */ @API(status = API.Status.STABLE, since = "2.0.0") public abstract @NonNull Map<@NonNull String, @NonNull String> messages(); /** * Returns the message provider which is used to retrieve messages from message keys. *

* The keys are constants in {@link MinecraftHelp}. * * @return the message provider * @since 2.0.0 */ @Value.Default @API(status = API.Status.STABLE, since = "2.0.0") public @NonNull MessageProvider messageProvider() { return (sender, key, args) -> { final String message = this.messageOrDefault(key); if (args.isEmpty()) { return text(message); } return text(StringUtils.replaceAll( message, STRING_PLACEHOLDER_PATTERN, matchResult -> args.get(matchResult.group(1)))); }; } /** * Returns the colors used for help messages. *

* Defaults to {@link #DEFAULT_HELP_COLORS}. * * @return the active {@link HelpColors} * @since 2.0.0 */ @Value.Default @API(status = API.Status.STABLE, since = "2.0.0") public @NonNull HelpColors colors() { return DEFAULT_HELP_COLORS; } /** * Returns the length of the header/footer of help menus. *

* Defaults to {@link #DEFAULT_HEADER_FOOTER_LENGTH}. * * @return the length * @since 2.0.0 */ @Value.Default @API(status = API.Status.STABLE, since = "2.0.0") public @NonNegative int headerFooterLength() { return DEFAULT_HEADER_FOOTER_LENGTH; } /** * Returns the maximum number of help results to display on a page. *

* Defaults to {@link MinecraftHelp#DEFAULT_MAX_RESULTS_PER_PAGE}. * * @return the maximum number of results * @since 2.0.0 */ @Value.Default @API(status = API.Status.STABLE, since = "2.0.0") public @NonNegative int maxResultsPerPage() { return DEFAULT_MAX_RESULTS_PER_PAGE; } protected final @NonNull String messageOrDefault(final @NonNull String key) { return this.messages().getOrDefault(key, DEFAULT_MESSAGES.get(key)); } /** * Queries commands and send the results to the recipient. Will respect permissions. * * @param rawQuery command query (without leading '/', including optional page number) * @param recipient recipient */ public void queryCommands( final @NonNull String rawQuery, final @NonNull C recipient ) { final String[] splitQuery = rawQuery.split(" "); int page; String query; try { final String pageText = splitQuery[splitQuery.length - 1]; page = Integer.parseInt(pageText); query = rawQuery.substring(0, Math.max(rawQuery.lastIndexOf(pageText) - 1, 0)); } catch (NumberFormatException e) { page = 1; query = rawQuery; } final Audience audience = this.audience(recipient); this.printTopic( recipient, query, page, this.helpHandler().query(HelpQuery.of(recipient, query)) ); } private void printTopic( final @NonNull C sender, final @NonNull String query, final int page, final @NonNull HelpQueryResult helpTopic ) { if (helpTopic instanceof IndexCommandResult) { this.printIndexHelpTopic(sender, query, page, (IndexCommandResult) helpTopic); } else if (helpTopic instanceof MultipleCommandResult) { this.printMultiHelpTopic(sender, query, page, (MultipleCommandResult) helpTopic); } else if (helpTopic instanceof VerboseCommandResult) { this.printVerboseHelpTopic(sender, query, (VerboseCommandResult) helpTopic); } else { throw new IllegalArgumentException("Unknown help topic type"); } } private void printNoResults( final @NonNull C sender, final @NonNull String query ) { final Audience audience = this.audience(sender); audience.sendMessage(this.basicHeader(sender)); audience.sendMessage(LinearComponents.linear( this.messageProvider().provide(sender, MESSAGE_NO_RESULTS_FOR_QUERY).color(this.colors().text()), text(": \"", this.colors().text()), this.highlight(text("/" + query, this.colors().highlight())), text("\"", this.colors().text()) )); audience.sendMessage(this.footer(sender)); } private void printIndexHelpTopic( final @NonNull C sender, final @NonNull String query, final int page, final @NonNull IndexCommandResult helpTopic ) { if (helpTopic.isEmpty()) { this.printNoResults(sender, query); return; } final Audience audience = this.audience(sender); new Pagination>( (currentPage, maxPages) -> { final List header = new ArrayList<>(); header.add(this.paginatedHeader(sender, currentPage, maxPages)); header.add(this.showingResults(sender, query)); header.add(text() .append(this.lastBranch()) .append(space()) .append( this.messageProvider().provide( sender, MESSAGE_AVAILABLE_COMMANDS ).color(this.colors().text()) ) .append(text(":", this.colors().text())) .build() ); return header; }, (helpEntry, isLastOfPage) -> { final CommandDescription commandDescription = helpEntry.command().commandDescription(); final Component description; if (commandDescription.description() instanceof RichDescription) { description = ((RichDescription) commandDescription.description()).contents(); } else if (commandDescription.isEmpty()) { description = this.messageProvider().provide(sender, MESSAGE_CLICK_TO_SHOW_HELP); } else { description = this.descriptionDecorator().decorate( sender, commandDescription.description().textDescription() ); } final boolean lastBranch = isLastOfPage || helpTopic.entries().indexOf(helpEntry) == helpTopic.entries().size() - 1; return text() .append(text(" ")) .append(lastBranch ? this.lastBranch() : this.branch()) .append(this.highlight(text( String.format(" /%s", helpEntry.syntax()), this.colors().highlight() )) .hoverEvent(description.color(this.colors().text())) .clickEvent(runCommand(this.commandPrefix() + " " + helpEntry.syntax())) ) .build(); }, (currentPage, maxPages) -> this.paginatedFooter(sender, currentPage, maxPages, query), (attemptedPage, maxPages) -> this.pageOutOfRange(sender, attemptedPage, maxPages) ).render(helpTopic.entries(), page, this.maxResultsPerPage()).forEach(audience::sendMessage); } private void printMultiHelpTopic( final @NonNull C sender, final @NonNull String query, final int page, final @NonNull MultipleCommandResult helpTopic ) { if (helpTopic.childSuggestions().isEmpty()) { this.printNoResults(sender, query); return; } final Audience audience = this.audience(sender); final int headerIndentation = helpTopic.longestPath().length(); new Pagination( (currentPage, maxPages) -> { final List header = new ArrayList<>(); header.add(this.paginatedHeader(sender, currentPage, maxPages)); header.add(this.showingResults(sender, query)); header.add(this.lastBranch() .append(this.highlight(text(" /" + helpTopic.longestPath(), this.colors().highlight())))); return header; }, (suggestion, isLastOfPage) -> { final boolean lastBranch = isLastOfPage || helpTopic.childSuggestions().indexOf(suggestion) == helpTopic.childSuggestions().size() - 1; return ComponentHelper.repeat(space(), headerIndentation) .append(lastBranch ? this.lastBranch() : this.branch()) .append(this.highlight(text(" /" + suggestion, this.colors().highlight())) .hoverEvent(this.messageProvider().provide(sender, MESSAGE_CLICK_TO_SHOW_HELP) .color(this.colors().text())) .clickEvent(runCommand(this.commandPrefix() + " " + suggestion))); }, (currentPage, maxPages) -> this.paginatedFooter(sender, currentPage, maxPages, query), (attemptedPage, maxPages) -> this.pageOutOfRange(sender, attemptedPage, maxPages) ).render(helpTopic.childSuggestions(), page, this.maxResultsPerPage()).forEach(audience::sendMessage); } private void printVerboseHelpTopic( final @NonNull C sender, final @NonNull String query, final @NonNull VerboseCommandResult helpTopic ) { final Audience audience = this.audience(sender); audience.sendMessage(this.basicHeader(sender)); audience.sendMessage(this.showingResults(sender, query)); final String command = this.commandManager().commandSyntaxFormatter() .apply(sender, helpTopic.entry().command().components(), null); audience.sendMessage(text() .append(this.lastBranch()) .append(space()) .append(this.messageProvider().provide(sender, MESSAGE_COMMAND).color(this.colors().primary())) .append(text(": ", this.colors().primary())) .append(this.highlight(text("/" + command, this.colors().highlight()))) ); /* Topics will use the long description if available, but fall back to the short description. */ final Description commandDescription = helpTopic.entry().command().commandDescription().verboseDescription(); final Component topicDescription; if (commandDescription instanceof RichDescription) { topicDescription = ((RichDescription) commandDescription).contents(); } else if (commandDescription.isEmpty()) { topicDescription = this.messageProvider().provide(sender, MESSAGE_NO_DESCRIPTION); } else { topicDescription = this.descriptionDecorator().decorate( sender, commandDescription.textDescription() ); } final boolean hasArguments = helpTopic.entry().command().components().size() > 1; audience.sendMessage(text() .append(text(" ")) .append(hasArguments ? this.branch() : this.lastBranch()) .append(space()) .append(this.messageProvider().provide(sender, MESSAGE_DESCRIPTION).color(this.colors().primary())) .append(text(": ", this.colors().primary())) .append(topicDescription.color(this.colors().text())) ); if (hasArguments) { audience.sendMessage(text() .append(text(" ")) .append(this.lastBranch()) .append(space()) .append(this.messageProvider().provide(sender, MESSAGE_ARGUMENTS).color(this.colors().primary())) .append(text(":", this.colors().primary())) ); final Iterator> iterator = helpTopic.entry().command().components().iterator(); /* Skip the first one because it's the command literal */ iterator.next(); while (iterator.hasNext()) { final CommandComponent component = iterator.next(); final String syntax = this.commandManager().commandSyntaxFormatter() .apply(sender, Collections.singletonList(component), null); final TextComponent.Builder textComponent = text() .append(text(" ")) .append(iterator.hasNext() ? this.branch() : this.lastBranch()) .append(this.highlight(text(" " + syntax, this.colors().highlight()))); if (component.optional()) { textComponent.append(text(" (", this.colors().alternateHighlight())); textComponent.append( this.messageProvider().provide(sender, MESSAGE_OPTIONAL).color(this.colors().alternateHighlight()) ); textComponent.append(text(")", this.colors().alternateHighlight())); } final Description description = component.description(); if (!description.isEmpty()) { textComponent.append(text(" - ", this.colors().accent())); textComponent.append(this.formatDescription(sender, description).colorIfAbsent(this.colors().text())); } audience.sendMessage(textComponent); } } audience.sendMessage(this.footer(sender)); } private Component formatDescription(final C sender, final Description description) { if (description instanceof RichDescription) { return ((RichDescription) description).contents(); } else { return this.descriptionDecorator().decorate(sender, description.textDescription()); } } private @NonNull Component showingResults( final @NonNull C sender, final @NonNull String query ) { return text() .append(this.messageProvider().provide(sender, MESSAGE_SHOWING_RESULTS_FOR_QUERY).color(this.colors().text())) .append(text(": \"", this.colors().text())) .append(this.highlight(text("/" + query, this.colors().highlight()))) .append(text("\"", this.colors().text())) .build(); } private @NonNull Component button( final char icon, final @NonNull String command, final @NonNull Component hoverText ) { return text() .append(space()) .append(text('[', this.colors().accent())) .append(text(icon, this.colors().alternateHighlight())) .append(text(']', this.colors().accent())) .append(space()) .clickEvent(runCommand(command)) .hoverEvent(hoverText) .build(); } private @NonNull Component footer(final @NonNull C sender) { return this.paginatedFooter(sender, 1, 1, ""); } private @NonNull Component paginatedFooter( final @NonNull C sender, final int currentPage, final int maxPages, final @NonNull String query ) { final boolean firstPage = currentPage == 1; final boolean lastPage = currentPage == maxPages; if (firstPage && lastPage) { return this.line(this.headerFooterLength()); } final Component nextPageButton = this.button('→', this.pageCommand(query, currentPage + 1), this.messageProvider().provide(sender, MESSAGE_CLICK_FOR_NEXT_PAGE).color(this.colors().text()) ); if (firstPage) { return this.header(sender, nextPageButton); } final Component previousPageButton = this.button('←', this.pageCommand(query, currentPage - 1), this.messageProvider().provide(sender, MESSAGE_CLICK_FOR_PREVIOUS_PAGE).color(this.colors().text()) ); if (lastPage) { return this.header(sender, previousPageButton); } final Component buttons = text() .append(previousPageButton) .append(this.line(3)) .append(nextPageButton).build(); return this.header(sender, buttons); } private String pageCommand(final String query, final int page) { if (query.isEmpty()) { return String.format("%s %s", this.commandPrefix(), page); } return String.format("%s %s %s", this.commandPrefix(), query, page); } private @NonNull Component header( final @NonNull C sender, final @NonNull Component title ) { final int sideLength = (this.headerFooterLength() - ComponentHelper.length(title)) / 2; return text() .append(this.line(sideLength)) .append(title) .append(this.line(sideLength)) .build(); } private @NonNull Component basicHeader(final @NonNull C sender) { return this.header(sender, LinearComponents.linear( space(), this.messageProvider().provide(sender, MESSAGE_HELP_TITLE).color(this.colors().highlight()), space() )); } private @NonNull Component paginatedHeader( final @NonNull C sender, final int currentPage, final int pages ) { return this.header(sender, text() .append(space()) .append(this.messageProvider().provide(sender, MESSAGE_HELP_TITLE).color(this.colors().highlight())) .append(space()) .append(text("(", this.colors().alternateHighlight())) .append(text(currentPage, this.colors().text())) .append(text("/", this.colors().alternateHighlight())) .append(text(pages, this.colors().text())) .append(text(")", this.colors().alternateHighlight())) .append(space()) .build() ); } private @NonNull Component line(final int length) { return ComponentHelper.repeat( text("-", this.colors().primary(), TextDecoration.STRIKETHROUGH), length ); } private @NonNull Component branch() { return text("├─", this.colors().accent()); } private @NonNull Component lastBranch() { return text("└─", this.colors().accent()); } private @NonNull Component highlight(final @NonNull Component component) { return ComponentHelper.highlight(component, this.colors().alternateHighlight()); } private @NonNull Component pageOutOfRange( final @NonNull C sender, final int attemptedPage, final int maxPages ) { final Map args = new LinkedHashMap<>(); args.put("page", String.valueOf(attemptedPage)); args.put("max_pages", String.valueOf(maxPages)); return this.highlight( this.messageProvider().provide(sender, MESSAGE_PAGE_OUT_OF_RANGE, args).color(this.colors().text()) ); } /** * Creates a {@link MessageProvider} that delegates to the provided {@link CaptionRegistry} and * {@link ComponentCaptionFormatter}. Transforms message keys into {@link Caption captions} using * {@link Caption#of(String)} and the prefix {@code help.minecraft.}. * * @param registry caption registry * @param formatter caption formatter * @param command sender type * @return message provider * @see #defaultCaptionsProvider() */ public static MessageProvider captionMessageProvider( final CaptionRegistry registry, final ComponentCaptionFormatter formatter ) { return (sender, key, args) -> { final Caption caption = Caption.of("help.minecraft." + key); final String resolved = registry.caption(caption, sender); return formatter.formatCaption( caption, sender, resolved, args.entrySet().stream().map(e -> CaptionVariable.of(e.getKey(), e.getValue())).collect(Collectors.toList()) ); }; } @FunctionalInterface @API(status = API.Status.STABLE, since = "2.0.0") public interface MessageProvider { /** * Creates a component from a command sender, key, and arguments * * @param sender command sender * @param key message key (constants in {@link MinecraftHelp} * @param args args * @return component */ @NonNull Component provide( @NonNull C sender, @NonNull String key, @NonNull Map args ); /** * Creates a component from a command sender and key. * * @param sender command sender * @param key message key (constants in {@link MinecraftHelp} * @return component */ default @NonNull Component provide(final @NonNull C sender, final @NonNull String key) { return this.provide(sender, key, Collections.emptyMap()); } } @API(status = API.Status.STABLE, since = "2.0.0") @FunctionalInterface public interface DescriptionDecorator { /** * Return a description decorator that wraps the given {@code function}. * * @param the command sender type * @param function the function * @return the decorator */ static @NonNull DescriptionDecorator wrap(final @NonNull Function function) { return (sender, description) -> function.apply(description); } /** * Returns a description decorator that turns the description into a text component without any style. * * @param the command sender type * @return the decorator */ static @NonNull DescriptionDecorator text() { return (sender, description) -> Component.text(description); } /** * Turns the given {@code description} into an adventure {@link Component}. * * @param sender command sender * @param description description * @return component */ @NonNull Component decorate(@NonNull C sender, @NonNull String description); } /** * Creates a new {@link HelpColors} instance. * * @param primary the primary color for the color scheme * @param highlight the primary color used to highlight commands and queries * @param alternateHighlight the secondary color used to highlight commands and queries * @param text the color used for description text * @param accent the color used for accents and symbols * @return a new {@link HelpColors} instance */ public static @NonNull HelpColors helpColors( final @NonNull TextColor primary, final @NonNull TextColor highlight, final @NonNull TextColor alternateHighlight, final @NonNull TextColor text, final @NonNull TextColor accent ) { return HelpColorsImpl.of(primary, highlight, alternateHighlight, text, accent); } /** * Class for holding the {@link TextColor TextColors} used for help menus */ @ImmutableImpl @Value.Immutable public interface HelpColors { /** * Returns the configured primary color. * * @return the primary color for the color scheme */ @NonNull TextColor primary(); /** * Returns the configured highlight color. * * @return the primary color used to highlight commands and queries */ @NonNull TextColor highlight(); /** * Returns the configured alternate highlight color. * * @return the secondary color used to highlight commands and queries */ @NonNull TextColor alternateHighlight(); /** * Returns the configured text color. * * @return the color used for description text */ @NonNull TextColor text(); /** * Returns the configured accent color. * * @return the color used for accents and symbols */ @NonNull TextColor accent(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy