Skip to content

Commit

Permalink
Fix & update Previewable arguments
Browse files Browse the repository at this point in the history
- Removed `CommandAPIHandler#previewableArguments` and related methods
- Added `PreviewableCommandNode` for storing `Previewable` information directly in Brigadier's tree
- Tweak `NMS_1_19_Common_ChatPreviewHandler` to build previews from the node tree rather than by the node path

TODO: Should probably test these changes on a real server to verify. Also, another example of Mojang/brigadier#144 being annoying.
  • Loading branch information
willkroboth committed Jun 3, 2024
1 parent 62265f3 commit 77e9a09
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 108 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
import dev.jorel.commandapi.executors.CommandArguments;
import dev.jorel.commandapi.executors.ExecutionInfo;
import dev.jorel.commandapi.preprocessor.RequireField;
import dev.jorel.commandapi.wrappers.PreviewableFunction;

import java.util.concurrent.CompletableFuture;

Expand Down Expand Up @@ -93,7 +92,6 @@ public class CommandAPIHandler<Argument

final CommandAPIPlatform<Argument, CommandSender, Source> platform;
final Map<String, RegisteredCommand> registeredCommands; // Keep track of what has been registered for type checking
final Map<List<String>, Previewable<?, ?>> previewableArguments; // Arguments with previewable chat
static final Pattern NAMESPACE_PATTERN = Pattern.compile("[0-9a-z_.-]+");

private static CommandAPIHandler<?, ?, ?> instance;
Expand All @@ -105,7 +103,6 @@ public class CommandAPIHandler<Argument
protected CommandAPIHandler(CommandAPIPlatform<Argument, CommandSender, Source> platform) {
this.platform = platform;
this.registeredCommands = new LinkedHashMap<>(); // This should be a LinkedHashMap to preserve insertion order
this.previewableArguments = new HashMap<>();

CommandAPIHandler.instance = this;
}
Expand Down Expand Up @@ -629,78 +626,6 @@ public Object parseArgument(CommandContext<Source> cmdCtx, String key, Argument
}
}

////////////////////////////////////
// SECTION: Previewable Arguments //
////////////////////////////////////

/**
* Handles a previewable argument. This stores the path to the previewable argument
* in {@link CommandAPIHandler#previewableArguments} for runtime resolving
*
* @param previousArguments The list of arguments that came before this argument
* @param previewableArgument The {@link Previewable} argument
*/
public void addPreviewableArgument(List<Argument> previousArguments, Argument previewableArgument) {
if (!(previewableArgument instanceof Previewable<?, ?> previewable)) {
throw new IllegalArgumentException("An argument must implement Previewable to be added as previewable argument");
}

// Generate all paths to the argument
List<List<String>> paths = new ArrayList<>();
paths.add(new ArrayList<>());

// TODO: Fix this, the `appendToCommandPaths` method was removed
// A smarter way to get this information should exist
// It probably makes sense to make a custom CommandNode for PreviewableArgument
if(true) throw new IllegalStateException("TODO: Fix this method");

// for (Argument argument : previousArguments) {
// argument.appendToCommandPaths(paths);
// }
// previewableArgument.appendToCommandPaths(paths);

// Insert paths to our map
for (List<String> path : paths) {
previewableArguments.put(path, previewable);
}
}

/**
* Looks up the function to generate a chat preview for a path of nodes in the
* command tree. This is a method internal to the CommandAPI and isn't expected
* to be used by plugin developers (but you're more than welcome to use it as
* you see fit).
*
* @param path a list of Strings representing the path (names of command nodes)
* to (and including) the previewable argument
* @return a {@link PreviewableFunction} that takes in a {@link PreviewInfo} and returns a
* text Component. If such a function is not available, this will
* return a function that always returns null.
*/
@SuppressWarnings("unchecked")
public Optional<PreviewableFunction<?>> lookupPreviewable(List<String> path) {
final Previewable<?, ?> previewable = previewableArguments.get(path);
if (previewable != null) {
return (Optional<PreviewableFunction<?>>) (Optional<?>) previewable.getPreview();
} else {
return Optional.empty();
}
}

/**
* @param path a list of Strings representing the path (names of command nodes)
* to (and including) the previewable argument
* @return Whether a previewable is legacy (non-Adventure) or not
*/
public boolean lookupPreviewableLegacyStatus(List<String> path) {
final Previewable<?, ?> previewable = previewableArguments.get(path);
if (previewable != null && previewable.getPreview().isPresent()) {
return previewable.isLegacy();
} else {
return true;
}
}

/////////////////////////
// SECTION: Reflection //
/////////////////////////
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.tree.CommandNode;
import dev.jorel.commandapi.AbstractArgumentTree;
import dev.jorel.commandapi.CommandAPIHandler;
import dev.jorel.commandapi.CommandPermission;
import dev.jorel.commandapi.RegisteredCommand;
import dev.jorel.commandapi.commandnodes.PreviewableArgumentBuilder;
import dev.jorel.commandapi.commandnodes.UnnamedRequiredArgumentBuilder;
import dev.jorel.commandapi.exceptions.DuplicateNodeNameException;
import dev.jorel.commandapi.exceptions.GreedyArgumentException;
Expand Down Expand Up @@ -371,16 +373,9 @@ public <Source> NodeInformation<Source> addArgumentNodes(
List<Argument> previousArguments, List<String> previousArgumentNames,
Function<List<Argument>, Command<Source>> terminalExecutorCreator
) {
CommandAPIHandler<Argument, CommandSender, Source> handler = CommandAPIHandler.getInstance();

// Check preconditions
checkPreconditions(previousNodeInformation, previousArguments, previousArgumentNames);

// Handle previewable argument
if (this instanceof Previewable<?, ?>) {
handler.addPreviewableArgument(previousArguments, (Argument) this);
}

// Create node
ArgumentBuilder<Source, ?> rootBuilder = createArgumentBuilder(previousArguments, previousArgumentNames);

Expand Down Expand Up @@ -426,19 +421,29 @@ public <Source> void checkPreconditions(
CommandAPIHandler<Argument, CommandSender, Source> handler = CommandAPIHandler.getInstance();

// Create node and add suggestions
// Note: I would like to combine these two `build.suggests(...)` calls, but they are actually two unrelated
// methods since UnnamedRequiredArgumentBuilder does not extend RequiredArgumentBuilder (see
// UnnamedRequiredArgumentBuilder for why). If UnnamedRequiredArgumentBuilder *does* extend
// RequiredArgumentBuilder, please simplify this if statement, like what Literal#createArgumentBuilder does.
// Note: I would like to combine these `builder.suggests(...)` calls, but they are actually unrelated
// methods since UnnamedRequiredArgumentBuilder and PreviewableArgumentBuilder do not extend RequiredArgumentBuilder
// (see those classes for why). If this has been fixed and they do extend RequiredArgumentBuilder, please simplify
// this if statement, like what Literal#createArgumentBuilder does.
SuggestionProvider<Source> suggestions = handler.generateBrigadierSuggestions(previousArguments, (Argument) this);
ArgumentBuilder<Source, ?> rootBuilder;
if(isListed) {
if (this instanceof Previewable<?, ?> previewable) {
// Handle previewable argument
PreviewableArgumentBuilder<Source, ?> builder = PreviewableArgumentBuilder.previewableArgument(
nodeName, rawType,
previewable.getPreview().orElse(null), previewable.isLegacy(), isListed
);
builder.suggests(suggestions);

rootBuilder = builder;
} else if (isListed) {
RequiredArgumentBuilder<Source, ?> builder = RequiredArgumentBuilder.argument(nodeName, rawType);
builder.suggests(handler.generateBrigadierSuggestions(previousArguments, (Argument) this));
builder.suggests(suggestions);

rootBuilder = builder;
} else {
UnnamedRequiredArgumentBuilder<Source, ?> builder = UnnamedRequiredArgumentBuilder.unnamedArgument(nodeName, rawType);
builder.suggests(handler.generateBrigadierSuggestions(previousArguments, (Argument) this));
builder.suggests(suggestions);

rootBuilder = builder;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package dev.jorel.commandapi.commandnodes;

import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.builder.ArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.tree.CommandNode;

import dev.jorel.commandapi.arguments.Previewable;
import dev.jorel.commandapi.wrappers.PreviewableFunction;

/**
* A special type of {@link RequiredArgumentBuilder} for {@link Previewable} Arguments. Compared to the
* {@link RequiredArgumentBuilder}, this class builds a {@link PreviewableCommandNode}
*
* @param <Source> The Brigadier Source object for running commands.
* @param <T> The type returned when this argument is parsed.
*/
// We can't actually extend RequiredArgumentBuilder since its only constructor is private :(
// See https://github.com/Mojang/brigadier/pull/144
public class PreviewableArgumentBuilder<Source, T> extends ArgumentBuilder<Source, PreviewableArgumentBuilder<Source, T>> {
// Everything here is copied from RequiredArgumentBuilder, which is why it would be nice to extend that directly
private final String name;
private final ArgumentType<T> type;
private SuggestionProvider<Source> suggestionsProvider = null;

// `Previewable` information
private final PreviewableFunction<?> previewableFunction;
private final boolean legacy;
private final boolean isListed;

private PreviewableArgumentBuilder(String name, ArgumentType<T> type, PreviewableFunction<?> previewableFunction, boolean legacy, boolean isListed) {
this.name = name;
this.type = type;

this.previewableFunction = previewableFunction;
this.legacy = legacy;
this.isListed = isListed;
}

public static <Source, T> PreviewableArgumentBuilder<Source, T> previewableArgument(String name, ArgumentType<T> type, PreviewableFunction<?> previewableFunction, boolean legacy, boolean isListed) {
return new PreviewableArgumentBuilder<>(name, type, previewableFunction, legacy, isListed);
}

public PreviewableArgumentBuilder<Source, T> suggests(final SuggestionProvider<Source> provider) {
this.suggestionsProvider = provider;
return getThis();
}

public SuggestionProvider<Source> getSuggestionsProvider() {
return suggestionsProvider;
}

@Override
protected PreviewableArgumentBuilder<Source, T> getThis() {
return this;
}

public ArgumentType<T> getType() {
return type;
}

public String getName() {
return name;
}

public PreviewableCommandNode<Source, T> build() {
final PreviewableCommandNode<Source, T> result = new PreviewableCommandNode<Source, T>(
previewableFunction, legacy, isListed,
getName(), getType(),
getCommand(), getRequirement(), getRedirect(), getRedirectModifier(), isFork(), getSuggestionsProvider()
);

for (final CommandNode<Source> argument : getArguments()) {
result.addChild(argument);
}

return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package dev.jorel.commandapi.commandnodes;

import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;

import com.mojang.brigadier.Command;
import com.mojang.brigadier.RedirectModifier;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.context.CommandContextBuilder;
import com.mojang.brigadier.context.ParsedArgument;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.tree.ArgumentCommandNode;
import com.mojang.brigadier.tree.CommandNode;

import dev.jorel.commandapi.arguments.Previewable;
import dev.jorel.commandapi.wrappers.PreviewableFunction;

/**
* A special type of {@link ArgumentCommandNode} for {@link Previewable} arguments. Compared to the
* {@link ArgumentCommandNode}, this class also has the methods {@link #getPreview()} and {@link #isLegacy()},
* which are used when players try to use the chat preview feature.
*
* @param <Source> The Brigadier Source object for running commands.
* @param <T> The type returned when this argument is parsed.
*/
public class PreviewableCommandNode<Source, T> extends ArgumentCommandNode<Source, T> {
private final PreviewableFunction<?> preview;
private final boolean legacy;

// Instead of having a listed and unlisted copy of this class, we can just handle this with this boolean
private final boolean isListed;

public PreviewableCommandNode(
PreviewableFunction<?> preview, boolean legacy, boolean isListed,
String name, ArgumentType<T> type,
Command<Source> command, Predicate<Source> requirement, CommandNode<Source> redirect, RedirectModifier<Source> modifier, boolean forks, SuggestionProvider<Source> customSuggestions
) {
super(name, type, command, requirement, redirect, modifier, forks, customSuggestions);
this.preview = preview;
this.legacy = legacy;
this.isListed = isListed;
}

// Methods needed to generate a preview
public Optional<PreviewableFunction<?>> getPreview() {
return Optional.ofNullable(preview);
}

public boolean isLegacy() {
return legacy;
}

// If we are unlisted, then when parsed, don't add the argument result to the CommandContext
public boolean isListed() {
return isListed;
}

@Override
public void parse(StringReader reader, CommandContextBuilder<Source> contextBuilder) throws CommandSyntaxException {
// Copied from `super#parse`, but with listability added
int start = reader.getCursor();

T result = this.getType().parse(reader);
ParsedArgument<Source, T> parsed = new ParsedArgument<>(start, reader.getCursor(), result);

if(isListed) contextBuilder.withArgument(this.getName(), parsed);

contextBuilder.withNode(this, parsed.getRange());
}

// Typical ArgumentCommandNode methods, but make it our classes
// Mostly copied and inspired by the implementations for these methods in ArgumentCommandNode
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof PreviewableCommandNode<?, ?> other)) return false;

if (!Objects.equals(this.preview, other.preview)) return false;
if (this.legacy != other.legacy) return false;
if (this.isListed != other.isListed) return false;
return super.equals(other);
}

@Override
public int hashCode() {
int result = Objects.hash(this.preview, this.legacy, this.isListed);
result = 31*result + super.hashCode();
return result;
}

// TODO: Um, this currently doesn't work since PreviewableArgumentBuilder does not extend RequiredArgumentBuilder
// See PreviewableArgumentBuilder for why
// I hope no one tries to use this method!
// @Override
// public PreviewableArgumentBuilder<Source, T> createBuilder() {
// PreviewableArgumentBuilder<Source, T> builder = PreviewableArgumentBuilder.previewableArgument(getName(), getType(), preview, legacy, isListed);

// builder.requires(getRequirement());
// builder.forward(getRedirect(), getRedirectModifier(), isFork());
// if (getCommand() != null) {
// builder.executes(getCommand());
// }
// return builder;
// }
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public UnnamedArgumentCommandNode(String name, ArgumentType<T> type, Command<Sou
}

// A UnnamedArgumentCommandNode is mostly identical to a ArgumentCommandNode
// The only difference is that when a UnnamedArgument is parsed, it does not add its result to the CommandContext
// The only difference is that when an UnnamedArgument is parsed, it does not add its argument result to the CommandContext
@Override
public void parse(StringReader reader, CommandContextBuilder<Source> contextBuilder) throws CommandSyntaxException {
final int start = reader.getCursor();
Expand Down
Loading

0 comments on commit 77e9a09

Please sign in to comment.