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

personthecat.catlib.command.CommandRegistrationContext Maven / Gradle / Ivy

Go to download

Utilities for serialization, commands, noise generation, IO, and some new data types.

The newest version!
package personthecat.catlib.command;

import com.mojang.brigadier.Command;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import personthecat.catlib.command.annotations.ModCommand;
import personthecat.catlib.command.annotations.CommandBuilder;
import personthecat.catlib.data.ModDescriptor;
import personthecat.catlib.util.LibStringUtils;
import personthecat.catlib.util.McUtils;
import personthecat.fresult.Result;

import javax.annotation.CheckReturnValue;
import javax.annotation.ParametersAreNonnullByDefault;
import net.minecraft.class_124;
import net.minecraft.class_2168;
import net.minecraft.class_2583;
import net.minecraft.class_2585;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import static net.minecraft.class_2170.method_9247;
import static personthecat.catlib.util.Shorthand.f;

/**
 * A helper used for generating and registering commands for the current mod.
 * 

* To use this context, generate a series of {@link LibCommandBuilder}s and * pass them into the context via {@link #addCommand} after the context * has been constructed. *

 *     CommandRegistrationContext.forMod(MOD_DESCRIPTOR)
 *       .addCommand(createCommand1())
 *       .addCommand(createCommand2())
 *       ...
 * 

* Alternatively, the builders can be generated reflectively by parsing methods * annotated with {@link ModCommand} or {@link CommandBuilder}. See the * documentation inside of these classes for more information on using them. *

* Classes containing the annotated methods can be passed into the context and * processed automatically by calling {@link #addAllCommands(Class[])}. *

 *     CommandRegistrationContext.forMod(MOD_DESCRIPTOR)
 *       .addAllCommands(ModCommands.class, ClientModCommands.class)
 *       ...
 * 

* Once each of the builders for the current mod have been passed into the * context, calling {@link #registerAll()} will take care of generating the * Brigadier command trees and registering them via {@link LibCommandRegistrar}. *

 *     CommandRegistrationContext.forMod(MOD_DESCRIPTOR)
 *       ...
 *       .registerAll();
 * 

* In addition, a help command will generated for the current mod which can be * accessed by running {@code /<modid> []} in-game. *

* The generated help command will be automatically formatted so that each * command will be displayed nicely on its given page. To customize this, call * {@link #setUsageLineCount(int)}, {@link #setUsageLineLength(int)}, or even * {@link #setUsageCommandCount(int)} to configure the generated output. *

 *     CommandRegistrationContext.forMod(MOD_DESCRIPTOR)
 *       ...
 *       .setUsageCmdCount(3)
 *       .setUsageLineLength(40)
 *       .registerAll();
 * 

* While this context is active, the calling mod's {@link ModDescriptor} can be * accessed by calling {@link #getActiveMod()} or {@link #getActiveModOrThrow()}. * This method has been and can be leveraged behind the scenes to provide * implicit access to mod-specific details in the current thread. *

* For example, a few of the argument types provided by {@link CommandUtils} * accept an optional second parameter for specifying the {@link ModDescriptor}. * If this parameter is absent, it will be acquired implicitly from the active * registration context. *

*/ @SuppressWarnings({"unused", "UnusedReturnValue"}) @ParametersAreNonnullByDefault public class CommandRegistrationContext { /** The text formatting to be used for the command usage header. */ private static final class_2583 HEADER_STYLE = class_2583.field_24360 .method_10977(class_124.field_1060) .method_10982(true); /** The text formatting to be used for displaying command usage. */ private static final class_2583 USAGE_STYLE = class_2583.field_24360 .method_10977(class_124.field_1080); /** The header to be used by the help message / usage text. */ private static final String USAGE_HEADER = " --- {} Command Usage ({} / {}) ---"; /** The default number of characters per line on the help page. */ private static final int DEFAULT_USAGE_LINE_LENGTH = 60; /** The default number of lines before any help page is forcibly wrapped. */ private static final int DEFAULT_USAGE_LINE_COUNT = 15; /** The default preferred number of commands to display per help page. */ private static final int DEFAULT_USAGE_CMD_COUNT = 5; /** The name of the help argument */ private static final String HELP_ARGUMENT = "help"; /** The name of the optional help page argument. */ private static final String PAGE_ARGUMENT = "page"; /** Whichever mod is currently registering commands in this thread. */ private static final ThreadLocal ACTIVE_MOD = new ThreadLocal<>(); private final List commands; private final ModDescriptor mod; private int usageLineLength; private int usageLineCount; private int usageCmdCount; private CommandRegistrationContext(final ModDescriptor mod) { this.commands = new ArrayList<>(); this.mod = mod; this.usageLineLength = DEFAULT_USAGE_LINE_LENGTH; this.usageLineCount = DEFAULT_USAGE_LINE_COUNT; this.usageCmdCount = DEFAULT_USAGE_CMD_COUNT; } /** * Construct a new registration context when given the single required value * containing information about the current mod's ID, name, and config folders. *

* Once the registration context has been constructed, this object cab be * accessed again by calling {@link #getActiveMod()}. It will be available * until this context has been disposed, which occurs when calling either * {@link #registerAll()} or {@link #dispose()}. *

* Consider storing this object in a static field for any method which * requires it after the context has been disposed. *

* Alternatively, the descriptor can be accessed by every command registered * through this context wherever a {@link CommandContextWrapper} is provided. *

* * @param mod Data about the mod registering commands. * @return A new registration context accepting mod command builders. */ @CheckReturnValue public static CommandRegistrationContext forMod(final ModDescriptor mod) { return new CommandRegistrationContext(mod).setupActiveMod(mod); } /** * Takes care of establishing the active {@link ModDescriptor} in the current * thread. This method is called automatically wherever a new command registration * context is created. *

* Note that if a {@link ModDescriptor} already exists in the current thread * which does not match the provided descriptor, this is an error and the * program cannot recover. *

* You can avoid this problem by making sure to call either {@link #registerAll()} * or {@link #dispose()}; *

* * @throws AmbiguousContextException If a different mod has forgotten to dispose of * its registration context. * @param mod The descriptor of the mod registering commands. * @return this, for method chaining. */ private CommandRegistrationContext setupActiveMod(final ModDescriptor mod) { final ModDescriptor activeMod = ACTIVE_MOD.get(); if (activeMod == null) { ACTIVE_MOD.set(mod); } else if (!mod.getModId().equals(activeMod.getModId())) { throw new AmbiguousContextException(activeMod.getModId(), mod.getModId()); } return this; } /** * Provides the active mod's {@link ModDescriptor} which is available only * to the current thread. Note that once this context has been disposed, * the descriptor will no longer be available and this method will return * null. * * @return The active mod's {@link ModDescriptor}. */ @Nullable public static ModDescriptor getActiveMod() { return ACTIVE_MOD.get(); } /** * Variant of {@link #getActiveMod()} which asserts that an active registration * context must be available. If a context is not available, this is an error * and the program must exit. * * @return The active mod's {@link ModDescriptor}. */ @NotNull public static ModDescriptor getActiveModOrThrow() { return Objects.requireNonNull(ACTIVE_MOD.get(), "Active mod queried out of context."); } /** * Adds a {@link LibCommandBuilder} to be processed by the wrapper. This builder * will be used to generate a help command. Each of the provided commands will be * registered behind the scenes along with the generated help command after calling * {@link #registerAll()}. * * @param command The builder for the command being added. * @return this, for method chaining. */ public CommandRegistrationContext addCommand(final LibCommandBuilder command) { this.commands.add(command); return this; } /** * Variant of {@link #addCommand(LibCommandBuilder)} which can accept multiple * builders simultaneously. Note that each of these builders must be generated * after the context has been constructed. * * @param commands A series of command builders being added. * @return this, for method chaining. */ public CommandRegistrationContext addAllCommands(final Collection commands) { this.commands.addAll(commands); return this; } /** * Variant of {@link #addAllCommands(Collection)} which accepts a series of classes * containing methods annotated with {@link ModCommand} and {@link CommandBuilder}. * Note that each of these methods is allowed to be private, but must be static. *

* See the respective classes for more information on using these annotations. *

* * @param classes A series of class files containing the annotated methods. * @return this, for method chaining. */ public CommandRegistrationContext addAllCommands(final Class... classes) { this.commands.addAll(CommandClassEvaluator.getBuilders(this.mod, classes)); return this; } /** * Adds every command declared in the class of the given object. This allows command * classes to use instance methods while also supporting multi-arg constructors. * * @param instances The instance of a class containing annotated command definitions. * @return this, for method chaining. */ public CommandRegistrationContext addAllCommands(final Object... instances) { this.commands.addAll(CommandClassEvaluator.getBuilders(this.mod, instances)); return this; } /** * Adds the default set of commands provided by the library as sub commands of the * current mod's main command. Note that every command accepting a file argument * will be adjusted for the current mod according to its {@link ModDescriptor}. * * @return this, for method chaining. */ public CommandRegistrationContext addLibCommands() { this.commands.addAll(DefaultLibCommands.createAll(this.mod, false)); return this; } /** * Sets the maximum number of characters per line to be displayed in the output * of this mod's generated help command. * * @param length The maximum number of characters per line. * @return this, for method chaining. */ public CommandRegistrationContext setUsageLineLength(final int length) { this.usageLineLength = Math.min(100, Math.max(10, length)); return this; } /** * Sets the maximum number of lines to be displayed per page of this mod's * generated help command. If number of lines exceeds this limit, the next * command will be moved onto the following page. * * @param count The maximum number of lines per page. * @return this, for method chaining. */ public CommandRegistrationContext setUsageLineCount(final int count) { this.usageLineCount = Math.min(90, Math.max(1, count)); return this; } /** * Sets the maximum number of commands per page of this mod's generated help * command. Regardless of the line count, once the allotted number of commands * have been displayed on the current page, the following commands will be moved * onto the next page. * * @param count The maximum number of commands per page. * @return this, for method chaining. */ public CommandRegistrationContext setUsageCommandCount(final int count) { this.usageCmdCount = Math.min(30, Math.max(1, count)); return this; } /** * Generates this mod's help command and registers all of the commands * in this context with {@link LibCommandRegistrar}. *

* After the commands have been registered, the active mod descriptor will be * disposed of and can no longer be accessed. *

*/ public void registerAll() { final List commands = this.getCommandsForCurrentSide(); final LiteralArgumentBuilder modCommand = this.generateBasicModCommand(commands); for (final LibCommandBuilder command : commands) { if (command.getHelpInfo().isGlobal()) { LibCommandRegistrar.registerCommand(command.getCommand(), command.getSide()); } else { modCommand.then(command.getCommand()); } } LibCommandRegistrar.registerCommand(modCommand); this.dispose(); } /** * Manually clears the active mod descriptor from the current thread. After this * point, the mod's descriptor can no longer be access through the library except * when provided a {@link CommandContextWrapper}. */ public void dispose() { ACTIVE_MOD.remove(); } /** * Generates a list of every {@link LibCommandBuilder} which is valid for the current * server side. * * @return Every {@link LibCommandBuilder} applicable for the current context. */ public List getCommandsForCurrentSide() { final boolean dedicated = McUtils.isDedicatedServer(); return this.commands.stream() .filter(builder -> builder.getSide().canRegister(dedicated)) .collect(Collectors.toList()); } /** * Generates a basic {@link LiteralArgumentBuilder} named according to this mod's * command prefix, which is provided by its {@link ModDescriptor}. * * @param commands Every command applicable to the current server side. * @return A generated {@link LiteralArgumentBuilder} for the current mod. */ private LiteralArgumentBuilder generateBasicModCommand(final List commands) { final List message = this.createHelpMessage(commands); final Command helpCommand = this.createHelp(message); return method_9247(this.mod.getCommandPrefix()).executes(helpCommand) .then(method_9247(HELP_ARGUMENT).executes(helpCommand) .then(CommandUtils.arg(PAGE_ARGUMENT, 1, message.size()).executes(helpCommand))); } /** * Generates the basic help command to used by the current mod. * * @param pages The generated text components corresponding to each help page. * @return The command to be executed when provided an optional page number. */ private Command createHelp(final List pages) { return ctx -> { final class_2168 source = ctx.getSource(); source.method_9226(pages.get(this.getHelpPage(ctx) - 1), true); return 0; }; } /** * Gets the help page argument from the current {@link CommandContext}. * * @param ctx The regular command context exposed to Brigadier commands. * @return The requested page number, or else 1. */ private int getHelpPage(final CommandContext ctx) { return Result.suppress(() -> ctx.getArgument(PAGE_ARGUMENT, Integer.class)) .orElseGet(e -> 1); } /** * Generates a list of help pages when provided every command that is valid * for the current server side. * * @param commands Every command builder applicable for the current context. * @return The generated list of formatted help pages. */ private List createHelpMessage(final List commands) { final List> pages = this.createHelpPages(commands); final List messages = new ArrayList<>(); final boolean anyGlobal = this.isAnyGlobal(commands); for (int i = 0; i < pages.size(); i++) { final List page = pages.get(i); final class_2585 message = new class_2585(""); // No formatting. message.method_10852(this.createHeader(i + 1, pages.size())); for (final HelpCommandInfo info : page) { message.method_27693("\n").method_10852(this.createUsageText(anyGlobal, info)); } messages.add(message); } return messages; } /** * Sorts every {@link HelpCommandInfo} container from a list of command builders. * * @param commands Every command builder applicable for the current context. * @return The sorted pages of help info. */ private List> createHelpPages(final List commands) { final List> pages = new ArrayList<>(); List currentPage = new ArrayList<>(); int numLines = 0; int numCommands = 0; for (final LibCommandBuilder command : commands) { final HelpCommandInfo info = command.getHelpInfo(); if (info.getDescription().isEmpty()) { continue; } currentPage.add(info); numLines += 2 + info.getDescription().length() / this.usageLineLength; numCommands++; if (numLines >= this.usageLineCount || numCommands >= this.usageCmdCount) { pages.add(currentPage); currentPage = new ArrayList<>(); numLines = 0; numCommands = 0; } } pages.add(currentPage); return pages; } /** * Determines whether any of the commands in the given list should be applied to * the global root command node. * * @param commands Every command applicable for the current context. * @return whether any command in the list is global. */ private boolean isAnyGlobal(final List commands) { return commands.stream().anyMatch(c -> c.getHelpInfo().isGlobal()); } /** * Generates the header text to be rendered at the top of the current help page. * * @param page The current page number. * @param numPages The last page number. * @return The formatted header as a {@link class_2585}. */ private class_2585 createHeader(final int page, final int numPages) { return (class_2585) new class_2585(f(USAGE_HEADER, this.mod.getName(), page, numPages)) .method_10862(HEADER_STYLE); } /** * Generates the formatted usage text for a single command's {@link HelpCommandInfo}. * If any command in the context is global, every non-global command will be rendered * with /<modid> in the title. * * @param anyGlobal Whether any command in the context is a global command. * @param info The raw help info corresponding to the current command. * @return The formatted help info as a {@link class_2585}. */ private class_2585 createUsageText(final boolean anyGlobal, final HelpCommandInfo info) { final String prefix = info.isGlobal() || !anyGlobal ? "" : this.mod.getCommandPrefix() + " "; final String command = prefix + info.getName() + " " + info.getArguments(); final class_2585 text = new class_2585(command); final List lines = LibStringUtils.wrapLines(info.getDescription(), this.usageLineLength); text.method_10852(new class_2585(" :\n " + lines.get(0)).method_10862(USAGE_STYLE)); for (int i = 1; i < lines.size(); i++) { text.method_10852(new class_2585("\n " + lines.get(i)).method_10862(USAGE_STYLE)); } return text; } private static class AmbiguousContextException extends RuntimeException { AmbiguousContextException(final String... ids) { super("Multiple contexts in the current thread. Call #registerAll: " + Arrays.toString(ids)); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy