diff --git a/patches/minecraft/net/minecraft/client/gui/screens/Screen.java.patch b/patches/minecraft/net/minecraft/client/gui/screens/Screen.java.patch index f28c6450dc..2d9a13ab37 100644 --- a/patches/minecraft/net/minecraft/client/gui/screens/Screen.java.patch +++ b/patches/minecraft/net/minecraft/client/gui/screens/Screen.java.patch @@ -154,7 +154,7 @@ if (p_96614_) { this.f_96541_.f_91065_.m_93076_().m_93783_(p_96613_); } -+ //if (net.minecraftforge.client.ClientCommandHandler.instance.executeCommand(mc.player, msg) != 0) return; //Forge: TODO Client command re-write ++ if (net.minecraftforge.client.ClientCommandHandler.sendMessage(p_96613_)) return; this.f_96541_.f_91074_.m_108739_(p_96613_); } diff --git a/patches/minecraft/net/minecraft/client/multiplayer/ClientPacketListener.java.patch b/patches/minecraft/net/minecraft/client/multiplayer/ClientPacketListener.java.patch index 57f44253c8..0505658f70 100644 --- a/patches/minecraft/net/minecraft/client/multiplayer/ClientPacketListener.java.patch +++ b/patches/minecraft/net/minecraft/client/multiplayer/ClientPacketListener.java.patch @@ -50,6 +50,14 @@ if (p_194239_ instanceof CommandBlockEntity && this.f_104888_.f_91080_ instanceof CommandBlockEditScreen) { ((CommandBlockEditScreen)this.f_104888_.f_91080_).m_98398_(); +@@ -1139,6 +_,7 @@ + public void m_7443_(ClientboundCommandsPacket p_104990_) { + PacketUtils.m_131363_(p_104990_, this, this.f_104888_); + this.f_104899_ = new CommandDispatcher<>(p_104990_.m_131884_()); ++ this.f_104899_ = net.minecraftforge.client.ClientCommandHandler.mergeServerCommands(this.f_104899_); + } + + public void m_7183_(ClientboundStopSoundPacket p_105116_) { @@ -1160,6 +_,7 @@ clientrecipebook.m_90625_(this.f_104900_.m_44051_()); clientrecipebook.m_90639_().forEach(mutablesearchtree::m_8080_); diff --git a/patches/minecraft/net/minecraft/commands/CommandSourceStack.java.patch b/patches/minecraft/net/minecraft/commands/CommandSourceStack.java.patch new file mode 100644 index 0000000000..ee0b3d7979 --- /dev/null +++ b/patches/minecraft/net/minecraft/commands/CommandSourceStack.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/commands/CommandSourceStack.java ++++ b/net/minecraft/commands/CommandSourceStack.java +@@ -35,7 +_,7 @@ + import net.minecraft.world.phys.Vec2; + import net.minecraft.world.phys.Vec3; + +-public class CommandSourceStack implements SharedSuggestionProvider { ++public class CommandSourceStack implements SharedSuggestionProvider, net.minecraftforge.common.extensions.IForgeCommandSourceStack { + public static final SimpleCommandExceptionType f_81286_ = new SimpleCommandExceptionType(new TranslatableComponent("permissions.requires.player")); + public static final SimpleCommandExceptionType f_81287_ = new SimpleCommandExceptionType(new TranslatableComponent("permissions.requires.entity")); + private final CommandSource f_81288_; diff --git a/patches/minecraft/net/minecraft/commands/arguments/ObjectiveArgument.java.patch b/patches/minecraft/net/minecraft/commands/arguments/ObjectiveArgument.java.patch new file mode 100644 index 0000000000..bb927b88fc --- /dev/null +++ b/patches/minecraft/net/minecraft/commands/arguments/ObjectiveArgument.java.patch @@ -0,0 +1,20 @@ +--- a/net/minecraft/commands/arguments/ObjectiveArgument.java ++++ b/net/minecraft/commands/arguments/ObjectiveArgument.java +@@ -31,7 +_,7 @@ + + public static Objective m_101960_(CommandContext p_101961_, String p_101962_) throws CommandSyntaxException { + String s = p_101961_.getArgument(p_101962_, String.class); +- Scoreboard scoreboard = p_101961_.getSource().m_81377_().m_129896_(); ++ Scoreboard scoreboard = p_101961_.getSource().getScoreboard(); + Objective objective = scoreboard.m_83477_(s); + if (objective == null) { + throw f_101953_.create(s); +@@ -55,7 +_,7 @@ + + public CompletableFuture listSuggestions(CommandContext p_101974_, SuggestionsBuilder p_101975_) { + if (p_101974_.getSource() instanceof CommandSourceStack) { +- return SharedSuggestionProvider.m_82970_(((CommandSourceStack)p_101974_.getSource()).m_81377_().m_129896_().m_83474_(), p_101975_); ++ return SharedSuggestionProvider.m_82970_(((CommandSourceStack)p_101974_.getSource()).getScoreboard().m_83474_(), p_101975_); + } else if (p_101974_.getSource() instanceof SharedSuggestionProvider) { + SharedSuggestionProvider sharedsuggestionprovider = (SharedSuggestionProvider)p_101974_.getSource(); + return sharedsuggestionprovider.m_5497_((CommandContext)p_101974_, p_101975_); diff --git a/patches/minecraft/net/minecraft/commands/arguments/ResourceLocationArgument.java.patch b/patches/minecraft/net/minecraft/commands/arguments/ResourceLocationArgument.java.patch new file mode 100644 index 0000000000..9f755247ee --- /dev/null +++ b/patches/minecraft/net/minecraft/commands/arguments/ResourceLocationArgument.java.patch @@ -0,0 +1,20 @@ +--- a/net/minecraft/commands/arguments/ResourceLocationArgument.java ++++ b/net/minecraft/commands/arguments/ResourceLocationArgument.java +@@ -44,7 +_,7 @@ + + public static Advancement m_106987_(CommandContext p_106988_, String p_106989_) throws CommandSyntaxException { + ResourceLocation resourcelocation = p_106988_.getArgument(p_106989_, ResourceLocation.class); +- Advancement advancement = p_106988_.getSource().m_81377_().m_129889_().m_136041_(resourcelocation); ++ Advancement advancement = p_106988_.getSource().getAdvancement(resourcelocation); + if (advancement == null) { + throw f_106978_.create(resourcelocation); + } else { +@@ -53,7 +_,7 @@ + } + + public static Recipe m_106994_(CommandContext p_106995_, String p_106996_) throws CommandSyntaxException { +- RecipeManager recipemanager = p_106995_.getSource().m_81377_().m_129894_(); ++ RecipeManager recipemanager = p_106995_.getSource().getRecipeManager(); + ResourceLocation resourcelocation = p_106995_.getArgument(p_106996_, ResourceLocation.class); + return recipemanager.m_44043_(resourcelocation).orElseThrow(() -> { + return f_106979_.create(resourcelocation); diff --git a/patches/minecraft/net/minecraft/commands/arguments/TeamArgument.java.patch b/patches/minecraft/net/minecraft/commands/arguments/TeamArgument.java.patch new file mode 100644 index 0000000000..175989fd3e --- /dev/null +++ b/patches/minecraft/net/minecraft/commands/arguments/TeamArgument.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/commands/arguments/TeamArgument.java ++++ b/net/minecraft/commands/arguments/TeamArgument.java +@@ -28,7 +_,7 @@ + + public static PlayerTeam m_112091_(CommandContext p_112092_, String p_112093_) throws CommandSyntaxException { + String s = p_112092_.getArgument(p_112093_, String.class); +- Scoreboard scoreboard = p_112092_.getSource().m_81377_().m_129896_(); ++ Scoreboard scoreboard = p_112092_.getSource().getScoreboard(); + PlayerTeam playerteam = scoreboard.m_83489_(s); + if (playerteam == null) { + throw f_112085_.create(s); diff --git a/patches/minecraft/net/minecraft/commands/arguments/coordinates/BlockPosArgument.java.patch b/patches/minecraft/net/minecraft/commands/arguments/coordinates/BlockPosArgument.java.patch new file mode 100644 index 0000000000..9be17e4505 --- /dev/null +++ b/patches/minecraft/net/minecraft/commands/arguments/coordinates/BlockPosArgument.java.patch @@ -0,0 +1,14 @@ +--- a/net/minecraft/commands/arguments/coordinates/BlockPosArgument.java ++++ b/net/minecraft/commands/arguments/coordinates/BlockPosArgument.java +@@ -30,9 +_,9 @@ + + public static BlockPos m_118242_(CommandContext p_118243_, String p_118244_) throws CommandSyntaxException { + BlockPos blockpos = p_118243_.getArgument(p_118244_, Coordinates.class).m_119568_(p_118243_.getSource()); +- if (!p_118243_.getSource().m_81372_().m_46805_(blockpos)) { ++ if (!p_118243_.getSource().getUnsidedLevel().m_46805_(blockpos)) { + throw f_118234_.create(); +- } else if (!p_118243_.getSource().m_81372_().m_46739_(blockpos)) { ++ } else if (!p_118243_.getSource().getUnsidedLevel().m_46739_(blockpos)) { + throw f_118235_.create(); + } else { + return blockpos; diff --git a/src/main/java/net/minecraftforge/client/ClientCommandHandler.java b/src/main/java/net/minecraftforge/client/ClientCommandHandler.java new file mode 100644 index 0000000000..7deeb56ce9 --- /dev/null +++ b/src/main/java/net/minecraftforge/client/ClientCommandHandler.java @@ -0,0 +1,315 @@ +/* + * Minecraft Forge + * Copyright (c) 2016-2021. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation version 2.1 + * of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package net.minecraftforge.client; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.ParseResults; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +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 com.mojang.brigadier.tree.LiteralCommandNode; +import com.mojang.brigadier.tree.RootCommandNode; +import net.minecraft.ChatFormatting; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.commands.CommandRuntimeException; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.commands.synchronization.SuggestionProviders; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.ComponentUtils; +import net.minecraft.network.chat.HoverEvent; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.TextComponent; +import net.minecraft.network.chat.TranslatableComponent; +import net.minecraftforge.client.event.RegisterClientCommandsEvent; +import net.minecraftforge.common.MinecraftForge; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.function.Function; + +public class ClientCommandHandler +{ + private static final Logger LOGGER = LogManager.getLogger(); + private static CommandDispatcher commands = null; + + /* + * For internal use + * + * Merges command dispatcher use for suggestions to the command dispatcher used for client commands so they can be sent to the server, and vice versa so client commands appear + * with server commands in suggestions + */ + public static CommandDispatcher mergeServerCommands(CommandDispatcher serverCommands) + { + CommandDispatcher commandsTemp = new CommandDispatcher<>(); + MinecraftForge.EVENT_BUS.post(new RegisterClientCommandsEvent(commandsTemp)); + + // Copies the client commands into another RootCommandNode so that redirects can't be used with server commands + commands = new CommandDispatcher<>(); + copy(commandsTemp.getRoot(), commands.getRoot()); + + // Copies the server commands into another RootCommandNode so that redirects can't be used with client commands + RootCommandNode serverCommandsRoot = serverCommands.getRoot(); + CommandDispatcher newServerCommands = new CommandDispatcher<>(); + copy(serverCommandsRoot, newServerCommands.getRoot()); + + // Copies the server side commands into a temporary server side commands root node to be used later without the client commands + RootCommandNode serverCommandsCopy = new RootCommandNode<>(); + mergeCommandNode(newServerCommands.getRoot(), serverCommandsCopy, new IdentityHashMap<>(), + Minecraft.getInstance().getConnection().getSuggestionsProvider(), (context) -> 0, (suggestions) -> null); + + // Copies the client side commands into the server side commands to be used for suggestions + mergeCommandNode(commands.getRoot(), newServerCommands.getRoot(), new IdentityHashMap<>(), getSource(), (context) -> 0, (suggestions) -> { + SuggestionProvider suggestionProvider = SuggestionProviders + .safelySwap((SuggestionProvider) (SuggestionProvider) suggestions); + if (suggestionProvider == SuggestionProviders.ASK_SERVER) + { + suggestionProvider = (context, builder) -> { + ClientCommandSourceStack source = getSource(); + StringReader reader = new StringReader(context.getInput()); + if (reader.canRead() && reader.peek() == '/') + { + reader.skip(); + } + + ParseResults parse = commands.parse(reader, source); + return commands.getCompletionSuggestions(parse); + }; + } + return suggestionProvider; + }); + + // Copies the server side commands into the client side commands so that they can be sent to the server as a chat message + mergeCommandNode(serverCommandsCopy, commands.getRoot(), new IdentityHashMap<>(), Minecraft.getInstance().getConnection().getSuggestionsProvider(), + (context) -> { + Minecraft.getInstance().player.chat((context.getInput().startsWith("/") ? "" : "/") + context.getInput()); + return 0; + }, (suggestions) -> null); + return newServerCommands; + } + + /** + * @return The command dispatcher for client side commands + */ + public static CommandDispatcher getDispatcher() + { + return commands; + } + + /** + * @return A {@link ClientCommandSourceStack} for the player in the current client + */ + public static ClientCommandSourceStack getSource() + { + LocalPlayer player = Minecraft.getInstance().player; + return new ClientCommandSourceStack(player, player.position(), player.getRotationVector(), player.getPermissionLevel(), + player.getName().getString(), player.getDisplayName(), player); + } + + /** + * + * Creates a deep copy of the sourceNode while keeping the redirects referring to the old command tree + * + * @param sourceNode + * the original + * @param resultNode + * the result + */ + private static void copy(CommandNode sourceNode, CommandNode resultNode) + { + for (CommandNode child : sourceNode.getChildren()) + { + ArgumentBuilder builder = child.createBuilder(); + CommandNode copy = builder.build(); + copy(child, copy); + resultNode.addChild(copy); + } + } + + /** + * + * Deep copies the children of a command node and stores a link between the source and the copy + * + * @param sourceNode + * the original command node + * @param resultNode + * the result command node + * @param sourceToResult + * a map storing the original command node as the key and the result command node as the value + * @param canUse + * used to check if the player can use the command + * @param execute + * the command to execute in place of the old command + * @param sourceToResultSuggestion + * a function to convert from the {@link SuggestionProvider} with the original source stack to the {@link SuggestionProvider} with the result source stack + */ + private static void mergeCommandNode(CommandNode sourceNode, CommandNode resultNode, Map, CommandNode> sourceToResult, + S canUse, Command execute, Function, SuggestionProvider> sourceToResultSuggestion) + { + sourceToResult.put(sourceNode, resultNode); + for (CommandNode sourceChild : sourceNode.getChildren()) + { + if (sourceChild.canUse(canUse)) + { + resultNode.addChild(toResult(sourceChild, sourceToResult, canUse, execute, sourceToResultSuggestion)); + } + } + } + + /** + * + * Creates a deep copy of a command node with a different source stack + * + * @param sourceNode + * the original command node + * @param sourceToResult + * a map storing the original command node as the key and the result command node as the value + * @param canUse + * used to check if the player can use the command + * @param execute + * the command to execute in place of the old command + * @param sourceToResultSuggestion + * a function to convert from the {@link SuggestionProvider} with the original source stack to the {@link SuggestionProvider} with the result source stack + * @return the deep copied command node with the new source stack + */ + private static CommandNode toResult(CommandNode sourceNode, Map, CommandNode> sourceToResult, S canUse, Command execute, + Function, SuggestionProvider> sourceToResultSuggestion) + { + if (sourceToResult.containsKey(sourceNode)) + return sourceToResult.get(sourceNode); + + ArgumentBuilder resultBuilder; + if (sourceNode instanceof ArgumentCommandNode) + { + ArgumentCommandNode sourceArgument = (ArgumentCommandNode) sourceNode; + RequiredArgumentBuilder resultArgumentBuilder = RequiredArgumentBuilder.argument(sourceArgument.getName(), sourceArgument.getType()); + if (sourceArgument.getCustomSuggestions() != null) + { + resultArgumentBuilder.suggests(sourceToResultSuggestion.apply(sourceArgument.getCustomSuggestions())); + } + resultBuilder = resultArgumentBuilder; + } + else if (sourceNode instanceof LiteralCommandNode) + { + LiteralCommandNode sourceLiteral = (LiteralCommandNode) sourceNode; + resultBuilder = LiteralArgumentBuilder.literal(sourceLiteral.getLiteral()); + } + else if (sourceNode instanceof RootCommandNode) + { + CommandNode resultNode = new RootCommandNode<>(); + mergeCommandNode(sourceNode, resultNode, sourceToResult, canUse, execute, sourceToResultSuggestion); + return resultNode; + } + else + { + throw new IllegalStateException("Node type " + sourceNode + " is not a standard node type"); + } + + if (sourceNode.getCommand() != null) + { + resultBuilder.executes(execute); + } + + if (sourceNode.getRedirect() != null) + { + resultBuilder.redirect(toResult(sourceNode.getRedirect(), sourceToResult, canUse, execute, sourceToResultSuggestion)); + } + + CommandNode resultNode = resultBuilder.build(); + mergeCommandNode(sourceNode, resultNode, sourceToResult, canUse, execute, sourceToResultSuggestion); + return resultNode; + } + + /** + * Always try to execute the cached parsing of client message as a command. Requires that the execute field of the commands to be set to send to server so that they aren't + * treated as client command's that do nothing. + * + * {@link net.minecraft.commands.Commands#performCommand(CommandSourceStack, String)} for reference + * + * @param sendMessage + * the chat message + * @return false leaves the message to be sent to the server, true means it should be caught before {@link LocalPlayer#chat(String)} + */ + public static boolean sendMessage(String sendMessage) + { + StringReader reader = new StringReader(sendMessage); + + if (!reader.canRead() || reader.read() != '/') + { + return false; + } + + ClientCommandSourceStack source = getSource(); + + try + { + commands.execute(reader, source); + } + catch (CommandRuntimeException execution)// Probably thrown by the command + { + Minecraft.getInstance().player.sendMessage(new TextComponent("").append(execution.getComponent()).withStyle(ChatFormatting.RED), Util.NIL_UUID); + } + catch (CommandSyntaxException syntax)// Usually thrown by the CommandDispatcher + { + Minecraft.getInstance().player.sendMessage( + new TextComponent("").append(ComponentUtils.fromMessage(syntax.getRawMessage())).withStyle(ChatFormatting.RED), Util.NIL_UUID); + if (syntax.getInput() != null && syntax.getCursor() >= 0) + { + int position = Math.min(syntax.getInput().length(), syntax.getCursor()); + MutableComponent details = new TextComponent("") + .withStyle(ChatFormatting.GRAY) + .withStyle((style) -> style + .withClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, reader.getString()))); + if (position > 10) + { + details.append("..."); + } + details.append(syntax.getInput().substring(Math.max(0, position - 10), position)); + if (position < syntax.getInput().length()) + { + details.append(new TextComponent(syntax.getInput().substring(position)).withStyle(ChatFormatting.RED, ChatFormatting.UNDERLINE)); + } + details.append(new TranslatableComponent("command.context.here").withStyle(ChatFormatting.RED, ChatFormatting.ITALIC)); + Minecraft.getInstance().player.sendMessage(new TextComponent("").append(details).withStyle(ChatFormatting.RED), Util.NIL_UUID); + } + } + catch (Exception generic)// Probably thrown by the command + { + TextComponent message = new TextComponent(generic.getMessage() == null ? generic.getClass().getName() : generic.getMessage()); + Minecraft.getInstance().player.sendMessage(new TranslatableComponent("command.failed") + .withStyle(ChatFormatting.RED) + .withStyle((style) -> style + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, message))), + Util.NIL_UUID); + LOGGER.error("Error executing client command \"{}\"", sendMessage, generic); + } + return true; + } +} diff --git a/src/main/java/net/minecraftforge/client/ClientCommandSourceStack.java b/src/main/java/net/minecraftforge/client/ClientCommandSourceStack.java new file mode 100644 index 0000000000..de19c39bff --- /dev/null +++ b/src/main/java/net/minecraftforge/client/ClientCommandSourceStack.java @@ -0,0 +1,167 @@ +/* + * Minecraft Forge + * Copyright (c) 2016-2021. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation version 2.1 + * of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package net.minecraftforge.client; + +import net.minecraft.Util; +import net.minecraft.advancements.Advancement; +import net.minecraft.client.Minecraft; +import net.minecraft.commands.CommandSource; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.RegistryAccess; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec2; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.scores.Scoreboard; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * overrides for {@link CommandSourceStack} so that the methods will run successfully client side + */ +public class ClientCommandSourceStack extends CommandSourceStack +{ + + public ClientCommandSourceStack(CommandSource source, Vec3 position, Vec2 rotation, int permission, String plainTextName, Component displayName, + Entity executing) + { + super(source, position, rotation, null, permission, plainTextName, displayName, null, executing); + } + + /** + * Sends a success message without attempting to get the server side list of admins + */ + @Override + public void sendSuccess(Component message, boolean sendToAdmins) + { + Minecraft.getInstance().player.sendMessage(message, Util.NIL_UUID); + } + + /** + * Gets the list of teams from the client side + */ + @Override + public Collection getAllTeams() + { + return Minecraft.getInstance().level.getScoreboard().getTeamNames(); + } + + /** + * Gets the list of online player names from the client side + */ + @Override + public Collection getOnlinePlayerNames() + { + return Minecraft.getInstance().getConnection().getOnlinePlayers().stream().map((player) -> player.getProfile().getName()).collect(Collectors.toList()); + } + + /** + * Gets a {@link Stream} of recipe ids that are available on the client + */ + @Override + public Stream getRecipeNames() + { + return Minecraft.getInstance().getConnection().getRecipeManager().getRecipeIds(); + } + + /** + * Gets a set of {@link ResourceKey} for levels from the client side + */ + @Override + public Set> levels() + { + return Minecraft.getInstance().getConnection().levels(); + } + + /** + * Gets the {@link RegistryAccess} from the client side + */ + @Override + public RegistryAccess registryAccess() + { + return Minecraft.getInstance().getConnection().registryAccess(); + } + + /** + * Gets the scoreboard from the client side + */ + @Override + public Scoreboard getScoreboard() + { + return Minecraft.getInstance().level.getScoreboard(); + } + + /** + * Gets the advancement from the id from the client side where the advancement needs to be visible to the player + */ + @Override + public Advancement getAdvancement(ResourceLocation id) + { + return Minecraft.getInstance().getConnection().getAdvancements().getAdvancements().get(id); + } + + /** + * Gets the {@link RecipeManager} from the client side + */ + @Override + public RecipeManager getRecipeManager() + { + return Minecraft.getInstance().getConnection().getRecipeManager(); + } + + /** + * Gets the level from the client side + */ + @Override + public Level getUnsidedLevel() + { + return Minecraft.getInstance().level; + } + + /** + * @throws UnsupportedOperationException + * because the server isn't available on the client + */ + @Override + public MinecraftServer getServer() + { + throw new UnsupportedOperationException("Attempted to get server in client command"); + } + + /** + * @throws UnsupportedOperationException + * because the server side level isn't available on the client side + */ + @Override + public ServerLevel getLevel() + { + throw new UnsupportedOperationException("Attempted to get server level in client command"); + } + +} diff --git a/src/main/java/net/minecraftforge/client/event/RegisterClientCommandsEvent.java b/src/main/java/net/minecraftforge/client/event/RegisterClientCommandsEvent.java new file mode 100644 index 0000000000..52e53da3e4 --- /dev/null +++ b/src/main/java/net/minecraftforge/client/event/RegisterClientCommandsEvent.java @@ -0,0 +1,54 @@ +/* + * Minecraft Forge + * Copyright (c) 2016-2021. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation version 2.1 + * of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package net.minecraftforge.client.event; + +import com.mojang.brigadier.CommandDispatcher; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.ObjectiveArgument; +import net.minecraft.commands.arguments.ResourceLocationArgument; +import net.minecraftforge.eventbus.api.Event; + +/** + * Register commands to be executed on the client using this event. + * + * Some arguments behave differently: + *
    + *
  • {@link ResourceLocationArgument#getAdvancement(com.mojang.brigadier.context.CommandContext, String)} only returns advancements that are shown on the advancements screen + *
  • {@link ObjectiveArgument#getObjective(com.mojang.brigadier.context.CommandContext, String)} only returns objectives that are set to display + *
+ */ +public class RegisterClientCommandsEvent extends Event +{ + + private final CommandDispatcher dispatcher; + + public RegisterClientCommandsEvent(CommandDispatcher dispatcher) + { + this.dispatcher = dispatcher; + } + + /** + * @return The command dispatcher for registering commands to be executed on the client + */ + public CommandDispatcher getDispatcher() + { + return dispatcher; + } +} diff --git a/src/main/java/net/minecraftforge/common/extensions/IForgeCommandSourceStack.java b/src/main/java/net/minecraftforge/common/extensions/IForgeCommandSourceStack.java new file mode 100644 index 0000000000..d1f96bc6dd --- /dev/null +++ b/src/main/java/net/minecraftforge/common/extensions/IForgeCommandSourceStack.java @@ -0,0 +1,72 @@ +/* + * Minecraft Forge + * Copyright (c) 2016-2021. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation version 2.1 + * of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package net.minecraftforge.common.extensions; + +import net.minecraft.advancements.Advancement; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.level.Level; +import net.minecraft.world.scores.Scoreboard; + +/** + * Additional methods for {@link CommandSourceStack} so that commands and arguments can access various things without directly referencing using server specific classes + */ +public interface IForgeCommandSourceStack +{ + + private CommandSourceStack self() + { + return (CommandSourceStack) this; + } + + /** + * @return the scoreboard + */ + default Scoreboard getScoreboard() + { + return self().getServer().getScoreboard(); + } + + /** + * @return the advancement from the id + */ + default Advancement getAdvancement(ResourceLocation id) + { + return self().getServer().getAdvancements().getAdvancement(id); + } + + /** + * @return the recipe manager + */ + default RecipeManager getRecipeManager() + { + return self().getServer().getRecipeManager(); + } + + /** + * @return the level but without being specifically the server side level + */ + default Level getUnsidedLevel() + { + return self().getLevel(); + } + +} diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg index cbb524a262..b30114c5c5 100644 --- a/src/main/resources/META-INF/accesstransformer.cfg +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -74,6 +74,7 @@ public net.minecraft.client.model.geom.LayerDefinitions f_171107_ # INNER_ARMOR_ public net.minecraft.client.particle.ParticleEngine m_107378_(Lnet/minecraft/core/particles/ParticleType;Lnet/minecraft/client/particle/ParticleEngine$SpriteParticleRegistration;)V # register public net.minecraft.client.particle.ParticleEngine m_107381_(Lnet/minecraft/core/particles/ParticleType;Lnet/minecraft/client/particle/ParticleProvider;)V # register public net.minecraft.client.particle.ParticleEngine$SpriteParticleRegistration +public net.minecraft.client.player.LocalPlayer m_8088_()I #getPermissionLevel public net.minecraft.client.renderer.GameRenderer m_109128_(Lnet/minecraft/resources/ResourceLocation;)V # loadEffect protected-f net.minecraft.client.renderer.RenderStateShard f_110131_ # setupState public net.minecraft.client.renderer.RenderStateShard$BooleanStateShard diff --git a/src/test/java/net/minecraftforge/debug/chat/ClientCommandTest.java b/src/test/java/net/minecraftforge/debug/chat/ClientCommandTest.java new file mode 100644 index 0000000000..e6c8ee16fd --- /dev/null +++ b/src/test/java/net/minecraftforge/debug/chat/ClientCommandTest.java @@ -0,0 +1,143 @@ +/* + * Minecraft Forge + * Copyright (c) 2016-2021. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation version 2.1 + * of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package net.minecraftforge.debug.chat; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.commands.arguments.ObjectiveArgument; +import net.minecraft.commands.arguments.ResourceLocationArgument; +import net.minecraft.commands.arguments.TeamArgument; +import net.minecraft.commands.arguments.coordinates.BlockPosArgument; +import net.minecraft.commands.synchronization.SuggestionProviders; +import net.minecraft.network.chat.TextComponent; +import net.minecraftforge.client.event.RegisterClientCommandsEvent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.registries.ForgeRegistries; + +import java.util.List; + +@Mod("client_command_test") +public class ClientCommandTest +{ + public ClientCommandTest() + { + MinecraftForge.EVENT_BUS.addListener(this::init); + } + + private void init(RegisterClientCommandsEvent event) + { + event.getDispatcher().register( + Commands.literal("clientcommandtest") + // Used for checking suggestion providers that aren't registered + .then(Commands.literal("rawsuggest") + .then(Commands.argument("block", ResourceLocationArgument.id()) + .suggests((c, b) -> SharedSuggestionProvider.suggestResource(ForgeRegistries.BLOCKS.getKeys(), b)) + .executes(this::testCommand))) + // Used for checking suggestion providers that are registered + .then(Commands.literal("registeredsuggest").then( + Commands.argument("block", ResourceLocationArgument.id()) + .suggests(SuggestionProviders.AVAILABLE_BIOMES) + .executes(this::testCommand))) + // Used for checking if attempting to get the server on the client side errors + .then(Commands.literal("server") + .executes((context) -> { + context.getSource().getServer(); + context.getSource().sendSuccess(new TextComponent("Successfully called getServer should have errored"), false); + return 1; + })) + // Used for checking if attempting to get the server level on the client side errors + .then(Commands.literal("level") + .executes((context) -> { + context.getSource().getLevel(); + context.getSource().sendSuccess(new TextComponent("Successfully called getLevel should have errored"), false); + return 1; + })) + // Used for checking if getting a known objective argument works on the client side + .then(Commands.literal("get_objective") + .then(Commands.argument("objective", ObjectiveArgument.objective()) + .executes((context) -> { + context.getSource().sendSuccess(new TextComponent("Regular: ") + .append(ObjectiveArgument.getObjective(context, "objective").getFormattedDisplayName()), false); + return 1; + }))) + // Used for checking if getting a known advancement works on the client side + .then(Commands.literal("get_advancement") + .then(Commands.argument("advancement", ResourceLocationArgument.id()) + .executes((context) -> { + context.getSource().sendSuccess(ResourceLocationArgument.getAdvancement(context, "advancement").getChatComponent(), + false); + return 1; + }))) + // Used for checking if getting a known recipe works on the client side + .then(Commands.literal("get_recipe") + .then(Commands.argument("recipe", ResourceLocationArgument.id()) + .executes((context) -> { + context.getSource() + .sendSuccess(ResourceLocationArgument.getRecipe(context, "recipe").getResultItem().getDisplayName(), false); + return 1; + }))) + // Used for checking if getting a team works on the client side + .then(Commands.literal("get_team") + .then(Commands.argument("team", TeamArgument.team()) + .executes((context) -> { + context.getSource().sendSuccess(TeamArgument.getTeam(context, "team").getFormattedDisplayName(), false); + return 1; + }))) + // Used for checking if a block position is valid works on the client side + .then(Commands.literal("get_loaded_blockpos") + .then(Commands.argument("blockpos", BlockPosArgument.blockPos()) + .executes((context) -> { + context.getSource() + .sendSuccess(new TextComponent(BlockPosArgument.getLoadedBlockPos(context, "blockpos").toString()), false); + return 1; + }))) + // Used for checking if a command can have a requirement + .then(Commands.literal("requires") + .requires((source) -> false) + .executes((context) -> { + context.getSource().sendSuccess(new TextComponent("Executed command"), false); + return 1; + }))); + + // Used for testing that client command redirects can only be used with client commands + LiteralArgumentBuilder fork = Commands.literal("clientcommandfork"); + fork.fork(event.getDispatcher().getRoot(), (context) -> List.of(context.getSource(), context.getSource())) + .executes((context) -> { + context.getSource().sendSuccess(new TextComponent("Executing forked command"), false); + return 1; + }); + event.getDispatcher().register(fork); + } + + private int testCommand(CommandContext context) + { + context.getSource().sendSuccess(new TextComponent("Input: " + ResourceLocationArgument.getId(context, "block")), false); + context.getSource().sendSuccess(new TextComponent("Teams: " + context.getSource().getAllTeams()), false); + context.getSource().sendSuccess(new TextComponent("Players: " + context.getSource().getOnlinePlayerNames()), false); + context.getSource().sendSuccess(new TextComponent("First recipe: " + context.getSource().getRecipeNames().findFirst().get()), false); + context.getSource().sendSuccess(new TextComponent("Levels: " + context.getSource().levels()), false); + context.getSource().sendSuccess(new TextComponent("Registry Access: " + context.getSource().registryAccess()), false); + return 0; + } +} diff --git a/src/test/resources/META-INF/mods.toml b/src/test/resources/META-INF/mods.toml index 2e11234306..dec32996f7 100644 --- a/src/test/resources/META-INF/mods.toml +++ b/src/test/resources/META-INF/mods.toml @@ -178,5 +178,7 @@ license="LGPL v2.1" modId="custom_particle_type_test" [[mods]] modId="ingredient_invalidation" +[[mods]] + modId="client_command_test" # ADD ABOVE THIS LINE