fabric: Begin work on commands manager for platform

This commit is contained in:
Zach Levis 2020-11-24 21:39:29 -08:00 committed by Jason
parent 0722bf6ead
commit eef98da9c9
16 changed files with 737 additions and 8 deletions

View file

@ -0,0 +1,152 @@
//
// MIT License
//
// Copyright (c) 2020 Alexander Söderberg & Contributors
//
// 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 cloud.commandframework.fabric;
import cloud.commandframework.CommandManager;
import cloud.commandframework.CommandTree;
import cloud.commandframework.brigadier.BrigadierManagerHolder;
import cloud.commandframework.brigadier.CloudBrigadierManager;
import cloud.commandframework.context.CommandContext;
import cloud.commandframework.execution.AsynchronousCommandExecutionCoordinator;
import cloud.commandframework.execution.CommandExecutionCoordinator;
import cloud.commandframework.meta.CommandMeta;
import cloud.commandframework.meta.SimpleCommandMeta;
import net.minecraft.server.command.CommandOutput;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.text.LiteralText;
import net.minecraft.util.math.Vec2f;
import net.minecraft.util.math.Vec3d;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.function.Function;
public class FabricCommandManager<C> extends CommandManager<C> implements BrigadierManagerHolder<C> {
private final Function<ServerCommandSource, C> commandSourceMapper;
private final Function<C, ServerCommandSource> backwardsCommandSourceMapper;
private final CloudBrigadierManager<C, ServerCommandSource> brigadierManager;
/**
* Create a command manager using native source types.
*
* @param execCoordinator Execution coordinator instance.
* @return a new command manager
* @see #FabricCommandManager(Function, Function, Function) for a more thorough explanation
*/
public static FabricCommandManager<ServerCommandSource> createNative(
final Function<CommandTree<ServerCommandSource>, CommandExecutionCoordinator<ServerCommandSource>> execCoordinator
) {
return new FabricCommandManager<>(execCoordinator, Function.identity(), Function.identity());
}
/**
* Create a new command manager instance.
*
* @param commandExecutionCoordinator Execution coordinator instance. The coordinator is in charge of executing incoming
* commands. Some considerations must be made when picking a suitable execution coordinator
* for your platform. For example, an entirely asynchronous coordinator is not suitable
* when the parsers used in that particular platform are not thread safe. If you have
* commands that perform blocking operations, however, it might not be a good idea to
* use a synchronous execution coordinator. In most cases you will want to pick between
* {@link CommandExecutionCoordinator#simpleCoordinator()} and
* {@link AsynchronousCommandExecutionCoordinator}
* @param commandSourceMapper Function that maps {@link ServerCommandSource} to the command sender type
* @param backwardsCommandSourceMapper Function that maps the command sender type to {@link ServerCommandSource}
*/
@SuppressWarnings("unchecked")
protected FabricCommandManager(
final @NonNull Function<@NonNull CommandTree<C>, @NonNull CommandExecutionCoordinator<C>> commandExecutionCoordinator,
final Function<ServerCommandSource, C> commandSourceMapper,
final Function<C, ServerCommandSource> backwardsCommandSourceMapper
) {
super(commandExecutionCoordinator, new FabricCommandRegistrationHandler<>());
this.commandSourceMapper = commandSourceMapper;
this.backwardsCommandSourceMapper = backwardsCommandSourceMapper;
// We're always brigadier
this.brigadierManager = new CloudBrigadierManager<>(this, () -> new CommandContext<>(
// This looks ugly, but it's what the server does when loading datapack functions in 1.16+
// See net.minecraft.server.function.FunctionLoader.reload for reference
this.commandSourceMapper.apply(new ServerCommandSource(
CommandOutput.DUMMY,
Vec3d.ZERO,
Vec2f.ZERO,
null,
4,
"",
LiteralText.EMPTY,
null,
null
)),
this.getCaptionRegistry()
));
((FabricCommandRegistrationHandler<C>) this.getCommandRegistrationHandler()).initialize(this);
}
/**
* Check if a sender has a certain permission.
*
* <p>The current implementation checks op level, pending a full Fabric permissions api.</p>
*
* @param sender Command sender
* @param permission Permission node
* @return whether the sender has the specified permission
*/
@Override
public boolean hasPermission(@NonNull final C sender, @NonNull final String permission) {
final ServerCommandSource source = this.backwardsCommandSourceMapper.apply(sender);
return source.hasPermissionLevel(source.getMinecraftServer().getOpPermissionLevel());
}
@Override
public final @NonNull CommandMeta createDefaultCommandMeta() {
return SimpleCommandMeta.empty();
}
/**
* Gets the mapper from a game {@link ServerCommandSource} to the manager's {@code C} type.
*
* @return Command source mapper
*/
public final @NonNull Function<@NonNull ServerCommandSource, @NonNull C> getCommandSourceMapper() {
return this.commandSourceMapper;
}
/**
* Gets the mapper from the manager's {@code C} type to a game {@link ServerCommandSource}.
*
* @return Command source mapper
*/
public final @NonNull Function<@NonNull C, @NonNull ServerCommandSource> getBackwardsCommandSourceMapper() {
return this.backwardsCommandSourceMapper;
}
@Override
public final @NonNull CloudBrigadierManager<C, ServerCommandSource> brigadierManager() {
return this.brigadierManager;
}
}

View file

@ -0,0 +1,92 @@
//
// MIT License
//
// Copyright (c) 2020 Alexander Söderberg & Contributors
//
// 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 cloud.commandframework.fabric;
import cloud.commandframework.Command;
import cloud.commandframework.arguments.StaticArgument;
import cloud.commandframework.internal.CommandRegistrationHandler;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.tree.CommandNode;
import com.mojang.brigadier.tree.RootCommandNode;
import net.fabricmc.fabric.api.command.v1.CommandRegistrationCallback;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* A registration handler for Fabric API.
*
* <p>If the command registration callback has already been called, this will attempt
* to register with the active server's command dispatcher.</p>
*
* @param <C> command sender type
*/
public final class FabricCommandRegistrationHandler<C> implements CommandRegistrationHandler {
private @MonotonicNonNull FabricCommandManager<C> commandManager;
private final Set<Command<C>> registeredCommands = ConcurrentHashMap.newKeySet();
private boolean commandRegistrationCalled;
void initialize(final FabricCommandManager<C> manager) {
this.commandManager = manager;
CommandRegistrationCallback.EVENT.register(this::registerAllCommands);
}
@Override
@SuppressWarnings("unchecked")
public boolean registerCommand(@NonNull final Command<?> command) {
if (this.commandRegistrationCalled) {
throw new IllegalStateException("too late!");
}
return this.registeredCommands.add((Command<C>) command);
}
private void registerAllCommands(final CommandDispatcher<ServerCommandSource> dispatcher, final boolean isDedicated) {
this.commandRegistrationCalled = true;
for (final Command<C> command : this.registeredCommands) {
registerCommand(dispatcher.getRoot(), command);
}
}
private void registerCommand(final RootCommandNode<ServerCommandSource> dispatcher, final Command<C> command) {
@SuppressWarnings("unchecked")
final StaticArgument<C> first = ((StaticArgument<C>) command.getArguments().get(0));
final CommandNode<ServerCommandSource> baseNode = this.commandManager.brigadierManager().createLiteralCommandNode(
first.getName(),
command,
(src, perm) -> this.commandManager.hasPermission(this.commandManager.getCommandSourceMapper().apply(src), perm),
true,
new FabricExecutor<>(this.commandManager));
dispatcher.addChild(baseNode);
for (final String alias : first.getAlternativeAliases()) {
dispatcher.addChild(CommandManager.literal(alias).redirect(baseNode).build());
}
}
}

View file

@ -0,0 +1,174 @@
//
// MIT License
//
// Copyright (c) 2020 Alexander Söderberg & Contributors
//
// 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 cloud.commandframework.fabric;
import cloud.commandframework.exceptions.ArgumentParseException;
import cloud.commandframework.exceptions.CommandExecutionException;
import cloud.commandframework.exceptions.InvalidCommandSenderException;
import cloud.commandframework.exceptions.InvalidSyntaxException;
import cloud.commandframework.exceptions.NoPermissionException;
import cloud.commandframework.exceptions.NoSuchCommandException;
import cloud.commandframework.execution.CommandResult;
import com.mojang.brigadier.Command;
import com.mojang.brigadier.context.CommandContext;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.text.ClickEvent;
import net.minecraft.text.HoverEvent;
import net.minecraft.text.LiteralText;
import net.minecraft.text.MutableText;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.concurrent.CompletionException;
import java.util.function.BiConsumer;
final class FabricExecutor<C> implements Command<ServerCommandSource> {
private static final Text NEWLINE = new LiteralText("\n");
private static final String MESSAGE_INTERNAL_ERROR = "An internal error occurred while attempting to perform this command.";
private static final String MESSAGE_NO_PERMS =
"I'm sorry, but you do not have permission to perform this command. "
+ "Please contact the server administrators if you believe that this is in error.";
private static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command. Type \"/help\" for help.";
private final FabricCommandManager<C> manager;
FabricExecutor(final @NonNull FabricCommandManager<C> manager) {
this.manager = manager;
}
@Override
public int run(final @NonNull CommandContext<ServerCommandSource> ctx) {
final ServerCommandSource source = ctx.getSource();
final String input = ctx.getInput().substring(1);
final C sender = this.manager.getCommandSourceMapper().apply(source);
this.manager.executeCommand(sender, input).whenComplete(this.getResultConsumer(source, sender));
return com.mojang.brigadier.Command.SINGLE_SUCCESS;
}
private @NonNull BiConsumer<@NonNull CommandResult<C>, ? super Throwable> getResultConsumer(
final @NonNull ServerCommandSource source,
final @NonNull C sender
) {
return (result, throwable) -> {
if (throwable != null) {
if (throwable instanceof CompletionException) {
throwable = throwable.getCause();
}
final Throwable finalThrowable = throwable;
if (throwable instanceof InvalidSyntaxException) {
this.manager.handleException(
sender,
InvalidSyntaxException.class,
(InvalidSyntaxException) throwable,
(c, e) ->
source.sendError(
new LiteralText("Invalid Command Syntax. Correct command syntax is: ")
.append(new LiteralText(e.getCorrectSyntax())
.styled(style -> style.withColor(Formatting.GRAY))))
);
} else if (throwable instanceof InvalidCommandSenderException) {
this.manager.handleException(
sender,
InvalidCommandSenderException.class,
(InvalidCommandSenderException) throwable,
(c, e) ->
source.sendError(new LiteralText(finalThrowable.getMessage()))
);
} else if (throwable instanceof NoPermissionException) {
this.manager.handleException(
sender,
NoPermissionException.class,
(NoPermissionException) throwable,
(c, e) -> source.sendError(new LiteralText(MESSAGE_NO_PERMS))
);
} else if (throwable instanceof NoSuchCommandException) {
this.manager.handleException(
sender,
NoSuchCommandException.class,
(NoSuchCommandException) throwable,
(c, e) -> source.sendError(new LiteralText(MESSAGE_UNKNOWN_COMMAND))
);
} else if (throwable instanceof ArgumentParseException) {
this.manager.handleException(
sender,
ArgumentParseException.class,
(ArgumentParseException) throwable,
(c, e) -> source.sendError(new LiteralText("Invalid Command Argument: ")
.append(new LiteralText(finalThrowable.getCause().getMessage())
.styled(style -> style.withColor(Formatting.GRAY))))
);
} else if (throwable instanceof CommandExecutionException) {
this.manager.handleException(
sender,
CommandExecutionException.class,
(CommandExecutionException) throwable,
(c, e) -> {
source.sendError(decorateHoverStacktrace(
new LiteralText(MESSAGE_INTERNAL_ERROR),
finalThrowable.getCause(),
sender
));
finalThrowable.getCause().printStackTrace();
}
);
} else {
source.sendError(decorateHoverStacktrace(
new LiteralText(MESSAGE_INTERNAL_ERROR),
throwable,
sender
));
throwable.printStackTrace();
}
}
};
}
private MutableText decorateHoverStacktrace(final MutableText input, final Throwable cause, final C sender) {
if (!this.manager.hasPermission(sender, "cloud.hover-stacktrace")) {
return input;
}
final StringWriter writer = new StringWriter();
cause.printStackTrace(new PrintWriter(writer));
final String stackTrace = writer.toString().replace("\t", " ");
return input.styled(style -> style
.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
new LiteralText(stackTrace)
.append(NEWLINE)
.append(new LiteralText(" Click to copy")
.styled(s2 -> s2
.withColor(Formatting.GRAY)
.withItalic(true)))
))
.withClickEvent(new ClickEvent(
ClickEvent.Action.COPY_TO_CLIPBOARD,
stackTrace
)));
}
}

View file

@ -0,0 +1,28 @@
//
// MIT License
//
// Copyright (c) 2020 Alexander Söderberg & Contributors
//
// 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.
//
/**
* Fabric API-based implementation of Cloud
*/
package cloud.commandframework.fabric;

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View file

@ -0,0 +1,21 @@
{
"schemaVersion": 1,
"id": "cloud",
"version": "${version}",
"name": "Cloud",
"description": "Command framework and dispatcher for the JVM",
"authors": [ "Alexander Söderberg" ],
"contact": {
"homepage": "https://commandframework.cloud/",
"sources": "https://github.com/Incendo/cloud"
},
"license": "MIT",
"icon": "assets/cloud/logo.png",
"depends": {
"fabricloader": ">=0.7.4",
"fabric-command-api-v1": "*",
"minecraft": ">=1.14"
}
}

View file

@ -0,0 +1,68 @@
//
// MIT License
//
// Copyright (c) 2020 Alexander Söderberg & Contributors
//
// 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 cloud.commandframework.fabric.testmod;
import cloud.commandframework.arguments.CommandArgument;
import cloud.commandframework.arguments.standard.IntegerArgument;
import cloud.commandframework.arguments.standard.StringArgument;
import cloud.commandframework.execution.CommandExecutionCoordinator;
import cloud.commandframework.fabric.FabricCommandManager;
import net.fabricmc.api.ModInitializer;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.text.LiteralText;
import net.minecraft.text.TextColor;
public final class FabricExample implements ModInitializer {
private static final CommandArgument<ServerCommandSource, String> NAME = StringArgument.of("name");
private static final CommandArgument<ServerCommandSource, Integer> HUGS = IntegerArgument.<ServerCommandSource>newBuilder("hugs")
.asOptionalWithDefault("1")
.build();
@Override
public void onInitialize() {
// Create a commands manager. We'll use native command source types for this.
final FabricCommandManager<ServerCommandSource> manager =
FabricCommandManager.createNative(CommandExecutionCoordinator.simpleCoordinator());
manager.command(manager.commandBuilder("cloudtest")
.argument(NAME)
.argument(HUGS)
.handler(ctx -> {
ctx.getSender().sendFeedback(new LiteralText("Hello, ")
.append(ctx.get(NAME))
.append(", hope you're doing well!")
.styled(style -> style.withColor(TextColor.fromRgb(0xAA22BB))), false);
ctx.getSender().sendFeedback(new LiteralText("Cloud would like to give you ")
.append(new LiteralText(String.valueOf(ctx.get(HUGS)))
.styled(style -> style.withColor(TextColor.fromRgb(0xFAB3DA))))
.append(" hug(s) <3")
.styled(style -> style.withBold(true)), false);
}));
}
}

View file

@ -0,0 +1,28 @@
//
// MIT License
//
// Copyright (c) 2020 Alexander Söderberg & Contributors
//
// 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.
//
/**
* A test mod for the fabric implementation of Cloud.
*/
package cloud.commandframework.fabric.testmod;

View file

@ -0,0 +1,28 @@
{
"schemaVersion": 1,
"id": "cloud-testmod",
"version": "${version}",
"name": "Cloud Test mod",
"description": "Command framework and dispatcher for the JVM",
"authors": [ "Alexander Söderberg" ],
"contact": {
"homepage": "https://commandframework.cloud/",
"sources": "https://github.com/Incendo/cloud"
},
"license": "MIT",
"icon": "assets/cloud/logo.png",
"entrypoints": {
"main": [
"cloud.commandframework.fabric.testmod.FabricExample"
]
},
"depends": {
"fabricloader": ">=0.7.4",
"fabric-command-api-v1": "*",
"minecraft": ">=1.14",
"cloud": "*"
}
}