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

org.incendo.cloud.minecraft.modded.parser.VanillaArgumentParsers Maven / Gradle / Ivy

//
// MIT License
//
// Copyright (c) 2024 Incendo
//
// 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 org.incendo.cloud.minecraft.modded.parser;

import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import net.minecraft.commands.CommandBuildContext;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.commands.arguments.EntityArgument;
import net.minecraft.commands.arguments.MessageArgument;
import net.minecraft.commands.arguments.TimeArgument;
import net.minecraft.commands.arguments.coordinates.BlockPosArgument;
import net.minecraft.commands.arguments.coordinates.ColumnPosArgument;
import net.minecraft.commands.arguments.coordinates.Vec2Argument;
import net.minecraft.commands.arguments.coordinates.Vec3Argument;
import net.minecraft.commands.arguments.item.ItemArgument;
import net.minecraft.commands.arguments.item.ItemInput;
import net.minecraft.commands.arguments.selector.EntitySelector;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.phys.Vec3;
import org.apiguardian.api.API;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.incendo.cloud.brigadier.parser.WrappedBrigadierParser;
import org.incendo.cloud.context.CommandContext;
import org.incendo.cloud.minecraft.modded.ModdedCommandContextKeys;
import org.incendo.cloud.minecraft.modded.data.Coordinates;
import org.incendo.cloud.minecraft.modded.data.Message;
import org.incendo.cloud.minecraft.modded.data.MinecraftTime;
import org.incendo.cloud.minecraft.modded.data.MultipleEntitySelector;
import org.incendo.cloud.minecraft.modded.data.MultiplePlayerSelector;
import org.incendo.cloud.minecraft.modded.data.SingleEntitySelector;
import org.incendo.cloud.minecraft.modded.data.SinglePlayerSelector;
import org.incendo.cloud.minecraft.modded.internal.ContextualArgumentTypeProvider;
import org.incendo.cloud.minecraft.modded.internal.EntitySelectorAccess;
import org.incendo.cloud.parser.ArgumentParseResult;
import org.incendo.cloud.parser.ArgumentParser;
import org.incendo.cloud.parser.ParserDescriptor;

/**
 * Parsers for Vanilla command argument types.
 */
public final class VanillaArgumentParsers {

    private VanillaArgumentParsers() {
    }

    /**
     * A parser that wraps Brigadier argument types which need a {@link CommandBuildContext}
     *
     * @param        sender type
     * @param        argument value type
     * @param factory   factory that creates these arguments
     * @param valueType value type of parsers output
     * @return the parser
     */
    public static  @NonNull ParserDescriptor contextualParser(
        final @NonNull Function> factory,
        final @NonNull Class valueType
    ) {
        return ParserDescriptor.of(new WrappedBrigadierParser<>(new ContextualArgumentTypeProvider<>(factory)), valueType);
    }

    /**
     * A parser for {@link ItemInput}.
     *
     * @param  command sender type
     * @return the parser
     */
    public static  @NonNull ParserDescriptor itemInput() {
        return contextualParser(ItemArgument::item, ItemInput.class);
    }

    /**
     * A parser for in-game time, in ticks.
     *
     * @param  sender type
     * @return a parser descriptor
     */
    public static  @NonNull ParserDescriptor timeParser() {
        ArgumentParser parser = new WrappedBrigadierParser(TimeArgument.time())
            .flatMapSuccess((ctx, val) -> ArgumentParseResult.successFuture(MinecraftTime.of(val)));

        return ParserDescriptor.of(parser, MinecraftTime.class);
    }

    /**
     * A parser for block coordinates.
     *
     * @param  sender type
     * @return a parser descriptor
     */
    public static  @NonNull ParserDescriptor blockPosParser() {
        ArgumentParser parser = new WrappedBrigadierParser(BlockPosArgument.blockPos())
            .flatMapSuccess(VanillaArgumentParsers::mapToCoordinates);

        return ParserDescriptor.of(parser, Coordinates.BlockCoordinates.class);
    }

    /**
     * A parser for column coordinates.
     *
     * @param  sender type
     * @return a parser descriptor
     */
    public static  @NonNull ParserDescriptor columnPosParser() {
        ArgumentParser parser = new WrappedBrigadierParser(ColumnPosArgument.columnPos())
            .flatMapSuccess(VanillaArgumentParsers::mapToCoordinates);

        return ParserDescriptor.of(parser, Coordinates.ColumnCoordinates.class);
    }

    /**
     * A parser for coordinates, relative or absolute, from 2 doubles for x and z,
     * with y always defaulting to 0.
     *
     * @param centerIntegers whether to center integers at x.5
     * @param             sender type
     * @return a parser descriptor
     */
    public static  @NonNull ParserDescriptor vec2Parser(final boolean centerIntegers) {
        ArgumentParser parser = new WrappedBrigadierParser(new Vec2Argument(centerIntegers))
            .flatMapSuccess(VanillaArgumentParsers::mapToCoordinates);

        return ParserDescriptor.of(parser, Coordinates.CoordinatesXZ.class);
    }

    /**
     * A parser for coordinates, relative or absolute, from 3 doubles.
     *
     * @param centerIntegers whether to center integers at x.5
     * @param             sender type
     * @return a parser descriptor
     */
    public static  @NonNull ParserDescriptor vec3Parser(final boolean centerIntegers) {
        ArgumentParser parser = new WrappedBrigadierParser(Vec3Argument.vec3(centerIntegers))
            .flatMapSuccess(VanillaArgumentParsers::mapToCoordinates);

        return ParserDescriptor.of(parser, Coordinates.class);
    }

    @SuppressWarnings("unchecked")
    private static  @NonNull CompletableFuture<@NonNull ArgumentParseResult> mapToCoordinates(
        final @NonNull CommandContext ctx,
        final net.minecraft.commands.arguments.coordinates.@NonNull Coordinates posArgument
    ) {
        return requireServer(
            ctx,
            serverCommandSource -> ArgumentParseResult.successFuture((O) new CoordinatesImpl(
                serverCommandSource,
                posArgument
            ))
        );
    }

    /**
     * A parser for {@link SinglePlayerSelector}.
     *
     * @param  sender type
     * @return a parser descriptor
     */
    public static  @NonNull ParserDescriptor singlePlayerSelectorParser() {
        ArgumentParser parser = new WrappedBrigadierParser(EntityArgument.player())
            .flatMapSuccess((ctx, entitySelector) -> requireServer(
                ctx,
                serverCommandSource -> handleCommandSyntaxExceptionAsFailure(
                    () -> ArgumentParseResult.success(new SinglePlayerSelectorImpl(
                        ((EntitySelectorAccess) entitySelector).inputString(),
                        entitySelector,
                        entitySelector.findSinglePlayer(serverCommandSource)
                    ))
                )
            ));

        return ParserDescriptor.of(parser, SinglePlayerSelector.class);
    }

    /**
     * A parser for {@link MultiplePlayerSelector}.
     *
     * @param  sender type
     * @return a parser descriptor
     */
    public static  @NonNull ParserDescriptor multiplePlayerSelectorParser() {
        ArgumentParser parser = new WrappedBrigadierParser(EntityArgument.players())
            .flatMapSuccess((ctx, entitySelector) -> requireServer(
                ctx,
                serverCommandSource -> handleCommandSyntaxExceptionAsFailure(
                    () -> ArgumentParseResult.success(new MultiplePlayerSelectorImpl(
                        ((EntitySelectorAccess) entitySelector).inputString(),
                        entitySelector,
                        entitySelector.findPlayers(serverCommandSource)
                    ))
                )
            ));

        return ParserDescriptor.of(parser, MultiplePlayerSelector.class);
    }

    /**
     * A parser for {@link SingleEntitySelector}.
     *
     * @param  sender type
     * @return a parser instance
     */
    public static  @NonNull ParserDescriptor singleEntitySelectorParser() {
        ArgumentParser parser = new WrappedBrigadierParser(EntityArgument.entity())
            .flatMapSuccess((ctx, entitySelector) -> requireServer(
                ctx,
                serverCommandSource -> handleCommandSyntaxExceptionAsFailure(
                    () -> ArgumentParseResult.success(new SingleEntitySelectorImpl(
                        ((EntitySelectorAccess) entitySelector).inputString(),
                        entitySelector,
                        entitySelector.findSingleEntity(serverCommandSource)
                    ))
                )
            ));

        return ParserDescriptor.of(parser, SingleEntitySelector.class);
    }

    /**
     * A parser for {@link MultipleEntitySelector}.
     *
     * @param  sender type
     * @return a parser instance
     */
    public static  @NonNull ParserDescriptor multipleEntitySelectorParser() {
        ArgumentParser parser = new WrappedBrigadierParser(EntityArgument.entities())
            .flatMapSuccess((ctx, entitySelector) -> requireServer(
                ctx,
                serverCommandSource -> handleCommandSyntaxExceptionAsFailure(
                    () -> ArgumentParseResult.success(new MultipleEntitySelectorImpl(
                        ((EntitySelectorAccess) entitySelector).inputString(),
                        entitySelector,
                        Collections.unmodifiableCollection(entitySelector.findEntities(serverCommandSource))
                    ))
                )
            ));

        return ParserDescriptor.of(parser, MultipleEntitySelector.class);
    }

    /**
     * A parser for {@link Message}.
     *
     * @param  sender type
     * @return a parser instance
     */
    public static  @NonNull ParserDescriptor messageParser() {
        ArgumentParser parser = new WrappedBrigadierParser(MessageArgument.message())
            .flatMapSuccess((ctx, format) -> requireServer(
                ctx,
                serverCommandSource -> handleCommandSyntaxExceptionAsFailure(
                    () -> ArgumentParseResult.success(MessageImpl.from(
                        serverCommandSource,
                        format,
                        true
                    ))
                )
            ));

        return ParserDescriptor.of(parser, Message.class);
    }

    @FunctionalInterface
    private interface CommandSyntaxExceptionThrowingParseResultSupplier {

        @NonNull ArgumentParseResult result() throws CommandSyntaxException;
    }

    private static  @NonNull CompletableFuture> handleCommandSyntaxExceptionAsFailure(
        final @NonNull CommandSyntaxExceptionThrowingParseResultSupplier resultSupplier
    ) {
        try {
            return CompletableFuture.completedFuture(resultSupplier.result());
        } catch (final CommandSyntaxException ex) {
            return ArgumentParseResult.failureFuture(ex);
        }
    }

    private static @NonNull IllegalStateException serverOnly() {
        return new IllegalStateException("This command argument type is server-only.");
    }

    private static  @NonNull CompletableFuture> requireServer(
        final @NonNull CommandContext context,
        final @NonNull Function>> resultFunction
    ) {
        final SharedSuggestionProvider nativeSource = context.get(ModdedCommandContextKeys.SHARED_SUGGESTION_PROVIDER);
        if (!(nativeSource instanceof CommandSourceStack) || isClientSource(nativeSource)) {
            return ArgumentParseResult.failureFuture(serverOnly());
        }
        return resultFunction.apply((CommandSourceStack) nativeSource);
    }

    /**
     * Checks whether a source is from client commands.
     *
     * @param sharedSuggestionProvider source
     * @return whether the source is from client commands
     */
    @API(status = API.Status.INTERNAL)
    public static boolean isClientSource(final SharedSuggestionProvider sharedSuggestionProvider) {
        // NeoForge extends CommandSourceStack with ClientCommandSourceStack; Fabric has its own SharedSuggestionProvider impl
        return !sharedSuggestionProvider.getClass().equals(CommandSourceStack.class);
    }

    private record MessageImpl(Collection mentionedEntities, Component contents) implements Message {
        static MessageImpl from(
            final @NonNull CommandSourceStack source,
            final MessageArgument.@NonNull Message message,
            final boolean useSelectors
        ) throws CommandSyntaxException {
            final Component contents = message.toComponent(source, useSelectors);
            final MessageArgument.Part[] selectors = message.parts();
            final Collection entities;
            if (!useSelectors || selectors.length == 0) {
                entities = Collections.emptySet();
            } else {
                entities = new HashSet<>();
                for (final MessageArgument.Part selector : selectors) {
                    entities.addAll(selector.selector()
                        .findEntities(source));
                }
            }

            return new MessageImpl(entities, contents);
        }
    }

    private record CoordinatesImpl(CommandSourceStack source, net.minecraft.commands.arguments.coordinates.Coordinates wrappedCoordinates)
        implements Coordinates,
        Coordinates.CoordinatesXZ,
        Coordinates.BlockCoordinates,
        Coordinates.ColumnCoordinates {

        @Override
        public @NonNull Vec3 position() {
            return this.wrappedCoordinates.getPosition(this.source);
        }

        @Override
        public @NonNull BlockPos blockPos() {
            return BlockPos.containing(this.position());
        }

        @Override
        public boolean isXRelative() {
            return this.wrappedCoordinates.isXRelative();
        }

        @Override
        public boolean isYRelative() {
            return this.wrappedCoordinates.isYRelative();
        }

        @Override
        public boolean isZRelative() {
            return this.wrappedCoordinates.isZRelative();
        }
    }

    private record SingleEntitySelectorImpl(
        String inputString, EntitySelector selector, Entity single
    ) implements SingleEntitySelector {}

    private record MultipleEntitySelectorImpl(
        String inputString, EntitySelector selector, Collection values
    ) implements MultipleEntitySelector {}

    private record SinglePlayerSelectorImpl(
        String inputString, EntitySelector selector, ServerPlayer single
    ) implements SinglePlayerSelector {}

    private record MultiplePlayerSelectorImpl(
        String inputString, EntitySelector selector, Collection values
    ) implements MultiplePlayerSelector {}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy