Skip to content

Commit

Permalink
Implement DynamicMultiLiteralArgument
Browse files Browse the repository at this point in the history
Acts like `MultiLiteralArgument`, but literals can be changed and differ for each CommandSender

Resolves #513

Uses Paper's `AsyncPlayerSendCommandsEvent` and Velocity's `PlayerAvailableCommandsEvent` to change the client's view of the command tree. Note that a similar event does not exist on Spigot, so the suggestions are not dynamic, though the parsing still is.

`DifferentClientNode` class added to handle creating client-server command tree de-syncs (I believe this can be used to make `FlagsArgument` platform-agnostic)
  • Loading branch information
willkroboth committed Aug 15, 2024
1 parent 07a4ba5 commit a53870a
Show file tree
Hide file tree
Showing 15 changed files with 689 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ public enum CommandAPIArgumentType {
*/
DIMENSION("minecraft:dimension"),

/**
* The DynamicMultiLiteralArgument
*/
DYNAMIC_MULTI_LITERAL,

/**
* The EnchantmentArgument
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package dev.jorel.commandapi.arguments;

import com.mojang.brigadier.builder.ArgumentBuilder;
import dev.jorel.commandapi.commandnodes.DynamicMultiLiteralArgumentBuilder;

import java.util.List;

public interface DynamicMultiLiteralArgumentCommon<Argument
/// @cond DOX
extends AbstractArgument<?, ?, ?, ?>
/// @endcond
, CommandSender> {
// DynamicMultiLiteralArgument info
@FunctionalInterface
interface LiteralsCreator<CommandSender> {
List<String> createLiterals(CommandSender sender);
}

LiteralsCreator<CommandSender> getLiteralsCreator();

///////////////////////////////////////////////////////////////////////////////////////////////////////
// LINKED METHODS //
// These automatically link to methods in AbstractArgument (make sure they have the same signature) //
///////////////////////////////////////////////////////////////////////////////////////////////////////

/**
* Links to {@link AbstractArgument#getNodeName()}.
*/
String getNodeName();

/**
* Links to {@link AbstractArgument#isListed()}.
*/
boolean isListed();

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// OVERRIDING METHODS //
// A DynamicMultiLiteralArgument has special logic that should override the implementations in AbstractArgument //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Overrides {@link AbstractArgument#createArgumentBuilder(List, List)}.
* <p>
* We want to use DynamicMultiLiteralArgumentBuilder rather than RequiredArgumentBuilder.
*/
default <Source> ArgumentBuilder<Source, ?> createArgumentBuilder(List<Argument> previousArguments, List<String> previousArgumentNames) {
String name = getNodeName();
boolean isListed = isListed();
LiteralsCreator<CommandSender> literalsCreator = getLiteralsCreator();

previousArguments.add((Argument) this);
if (isListed()) previousArgumentNames.add(name);

return DynamicMultiLiteralArgumentBuilder.dynamicMultiLiteral(name, isListed, literalsCreator);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package dev.jorel.commandapi.commandnodes;

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.CommandContext;
import com.mojang.brigadier.context.CommandContextBuilder;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import com.mojang.brigadier.tree.ArgumentCommandNode;
import com.mojang.brigadier.tree.CommandNode;
import dev.jorel.commandapi.CommandAPIHandler;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;

public abstract class DifferentClientNode<Source, T> extends ArgumentCommandNode<Source, T> {
// Rewrite node trees for the client
public static <Source> void rewriteAllChildren(Source client, CommandNode<Source> parent) {
// Copy the children, as we do expect them to be modified
List<CommandNode<Source>> children = new ArrayList<>(parent.getChildren());
for (CommandNode<Source> child : children) {
rewriteChild(client, parent, child);
}
}

public static <Source> void rewriteChild(Source client, CommandNode<Source> parent, CommandNode<Source> child) {
// Get the node creator
DifferentClientNode<Source, ?> clientNodeCreator = null;
if (child instanceof DifferentClientNode<?, ?>) {
// child is directly a DifferentClientNode
clientNodeCreator = (DifferentClientNode<Source, ?>) child;
} else if (child instanceof ArgumentCommandNode<Source, ?> argument && argument.getType() instanceof Type<?, ?>) {
// child was copied from a DifferentClientNode using `createBuilder` (real node hidden in ArgumentType)
Type<Source, ?> type = (Type<Source, ?>) argument.getType();
clientNodeCreator = type.node;
}

if (clientNodeCreator != null) {
// Get the new client nodes
List<CommandNode<Source>> clientNodes = clientNodeCreator.rewriteNodeForClient(client);

// Inject client node
Map<String, CommandNode<Source>> children = CommandAPIHandler.getCommandNodeChildren(parent);
children.remove(child.getName());
for (CommandNode<Source> clientNode : clientNodes) {
children.put(clientNode.getName(), clientNode);
}
}

// Modify all children
rewriteAllChildren(client, child);
}

// Node information
private final ArgumentType<T> type;

public DifferentClientNode(
String name, ArgumentType<T> type,
Command<Source> command, Predicate<Source> requirement,
CommandNode<Source> redirect, RedirectModifier<Source> modifier, boolean forks
) {
// Platforms often copy this node as a normal ArgumentCommandNode,
// but we can hide a reference to ourselves in the ArgumentType used
// when ArgumentCommandNode#createBuilder is called
super(name, new Type<Source, T>(), command, requirement, redirect, modifier, forks, null);

((Type<Source, T>) super.getType()).node = this;

// This type actually represents this argument when serializing nodes to json
// and also helps this argument blend in with ambiguity checks
this.type = type;
}

// Handle type nonsense
private static class Type<Source, T> implements ArgumentType<T> {
private DifferentClientNode<Source, T> node;

@Override
public T parse(StringReader stringReader) {
throw new IllegalStateException("Not supposed to be called");
}
}

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

@Override
public boolean isValidInput(String input) {
try {
StringReader reader = new StringReader(input);
this.type.parse(reader);
return !reader.canRead() || reader.peek() == ' ';
} catch (CommandSyntaxException var3) {
return false;
}
}

@Override
public Collection<String> getExamples() {
return this.type.getExamples();
}

@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof DifferentClientNode<?, ?> other)) return false;

return this.type.equals(other.type) && super.equals(other);
}

@Override
public int hashCode() {
int result = this.type.hashCode();
result = 31 * result + super.hashCode();
return result;
}

// Special client-side nodes

/**
* Transforms this node into one the client should see.
*
* @param client The client who the new node is for. If this is null,
* the generated node should just be for any general client.
* @return The version of this {@link CommandNode} the client should see. This is a list, so one server
* node may become many on the client. If the list is empty, the client will not see any nodes.
*/
public abstract List<CommandNode<Source>> rewriteNodeForClient(Source client);

// Require inheritors to redefine these methods from ArgumentCommandNode
@Override
public abstract void parse(StringReader reader, CommandContextBuilder<Source> contextBuilder) throws CommandSyntaxException;

@Override
public abstract CompletableFuture<Suggestions> listSuggestions(CommandContext<Source> context, SuggestionsBuilder builder) throws CommandSyntaxException;

@Override
public abstract String toString();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package dev.jorel.commandapi.commandnodes;

import com.mojang.brigadier.builder.ArgumentBuilder;
import com.mojang.brigadier.tree.CommandNode;
import dev.jorel.commandapi.arguments.DynamicMultiLiteralArgumentCommon.LiteralsCreator;

public class DynamicMultiLiteralArgumentBuilder<CommandSender, Source> extends ArgumentBuilder<Source, DynamicMultiLiteralArgumentBuilder<CommandSender, Source>> {
// Build
private final String name;
private final boolean isListed;
private final LiteralsCreator<CommandSender> literalsCreator;

public DynamicMultiLiteralArgumentBuilder(String name, boolean isListed, LiteralsCreator<CommandSender> literalsCreator) {
this.name = name;
this.isListed = isListed;
this.literalsCreator = literalsCreator;
}

public static <CommandSender, Source> DynamicMultiLiteralArgumentBuilder<CommandSender, Source> dynamicMultiLiteral(
String name, boolean isListed, LiteralsCreator<CommandSender> literalsCreator
) {
return new DynamicMultiLiteralArgumentBuilder<>(name, isListed, literalsCreator);
}

// Getters
@Override
protected DynamicMultiLiteralArgumentBuilder<CommandSender, Source> getThis() {
return this;
}

public String getName() {
return name;
}

public boolean isListed() {
return isListed;
}

public LiteralsCreator<CommandSender> getLiteralsCreator() {
return literalsCreator;
}

// Create node
@Override
public DynamicMultiLiteralCommandNode<CommandSender, Source> build() {
final DynamicMultiLiteralCommandNode<CommandSender, Source> result = new DynamicMultiLiteralCommandNode<>(
name, isListed, literalsCreator,
getCommand(), getRequirement(),
getRedirect(), getRedirectModifier(), isFork()
);

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

return result;
}
}
Loading

0 comments on commit a53870a

Please sign in to comment.