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

personthecat.catlib.command.CommandClassEvaluator Maven / Gradle / Ivy

Go to download

Utilities for serialization, commands, noise generation, IO, and some new data types.

The newest version!
package personthecat.catlib.command;

import com.google.common.primitives.*;
import com.mojang.brigadier.Command;
import com.mojang.brigadier.arguments.*;
import com.mojang.brigadier.builder.ArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.mutable.MutableInt;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import personthecat.catlib.command.annotations.CommandBuilder;
import personthecat.catlib.command.annotations.ModCommand;
import personthecat.catlib.command.annotations.Node;
import personthecat.catlib.command.arguments.ArgumentDescriptor;
import personthecat.catlib.command.arguments.EnumArgument;
import personthecat.catlib.command.arguments.ListArgumentBuilder;
import personthecat.catlib.command.arguments.RegistryArgument;
import personthecat.catlib.command.LibCommandBuilder.CommandGenerator;
import personthecat.catlib.command.function.CommandFunction;
import personthecat.catlib.data.ModDescriptor;
import personthecat.catlib.event.error.LibErrorContext;
import personthecat.catlib.exception.FormattedException;
import personthecat.catlib.exception.UncheckedFormattedException;
import personthecat.catlib.util.LibStringUtils;
import personthecat.catlib.util.Shorthand;
import personthecat.catlib.util.unsafe.CachingReflectionHelper;
import personthecat.fresult.functions.ThrowingBiConsumer;

import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import net.minecraft.class_1792;
import net.minecraft.class_2168;
import net.minecraft.class_2170;
import net.minecraft.class_2247;
import net.minecraft.class_2290;
import net.minecraft.class_2561;
import net.minecraft.class_2585;
import net.minecraft.class_2588;
import net.minecraft.class_2680;

import static personthecat.catlib.util.unsafe.CachingReflectionHelper.tryInstantiate;

public class CommandClassEvaluator {

    private static final String COMMAND_CATEGORY = "catlib.errorMenu.commands";
    private static final String GENERIC_ERROR = "catlib.errorText.commandClass";
    private static final String NO_PARAMETERS = "catlib.errorText.noParameters";
    private static final String EXPECTED_BUILDER = "catlib.errorText.expectedBuilder";
    private static final String COULD_NOT_INVOKE = "catlib.errorText.noInvoke";
    private static final String MISSING_COMPILE_ARG = "catlib.errorText.missingCompileArg";
    private static final String COMMAND_NODE = "catlib.errorText.commandNode";
    private static final String LIST_NODE = "catlib.errorText.listNode";
    private static final String DISPLAY_NODE = "catlib.errorText.displayNode";

    private static final Mapping[] AUTOMATIC_MAPPINGS = {
        new Mapping<>(class_2247.class, class_2680.class, class_2247::method_9494),
        new Mapping<>(class_2290.class, class_1792.class, class_2290::method_9785)
    };

    private final List builders;
    private final ModDescriptor mod;
    private Object instance;
    private Class clazz;

    private CommandClassEvaluator(final ModDescriptor mod) {
        this.builders = new ArrayList<>();
        this.mod = mod;
        this.instance = null;
    }

    public static List getBuilders(final ModDescriptor mod, final Class... classes) {
        final CommandClassEvaluator evaluator = new CommandClassEvaluator(mod);
        for (final Class c : classes) {
            evaluator.instance = null;
            evaluator.clazz = c;
            evaluator.addCommandBuilders();
            evaluator.addModCommands();
        }
        return evaluator.builders;
    }

    public static List getBuilders(final ModDescriptor mod, final Object... instances) {
        final CommandClassEvaluator evaluator = new CommandClassEvaluator(mod);
        for (final Object o : instances) {
            evaluator.instance = o;
            evaluator.clazz = o.getClass();
            evaluator.addCommandBuilders();
            evaluator.addModCommands();
        }
        return evaluator.builders;
    }

    private void addCommandBuilders() {
        forEachAnnotated(CommandBuilder.class, (m, a) -> {
            if (m.getParameterCount() > 0) {
                throw new CommandClassEvaluationException(NO_PARAMETERS, m);
            }
            if (!LibCommandBuilder.class.isAssignableFrom(m.getReturnType())) {
                throw new CommandClassEvaluationException(EXPECTED_BUILDER, m);
            }
            if (this.instance == null && !Modifier.isStatic(m.getModifiers())) {
                this.instance = CachingReflectionHelper.tryInstantiate(this.clazz);
            }
            builders.add(getValue(m));
        });
    }

    private void addModCommands() {
        forEachAnnotated(ModCommand.class, (m, a) -> {
            if (this.instance == null && !Modifier.isStatic(m.getModifiers())) {
                this.instance = CachingReflectionHelper.tryInstantiate(this.clazz);
            }
            builders.add(createBuilder(m, a));
        });
    }

    private  void forEachAnnotated(Class a, ThrowingBiConsumer command) {
        for (final Method m : this.clazz.getDeclaredMethods()) {
            final A annotation = m.getAnnotation(a);
            if (annotation != null) {
                try {
                    command.accept(m, annotation);
                } catch (final UncheckedFormattedException e) {
                    LibErrorContext.error(this.mod, e.getCause());
                } catch (final FormattedException e) {
                    LibErrorContext.error(this.mod, e);
                }
            }
        }
    }

    private LibCommandBuilder createBuilder(Method m, ModCommand a) {
        final List tokens = LibStringUtils.tokenize(m.getName());
        final CommandFunction cmd = createConsumer(this.instance, m);
        final List entries = createEntries(tokens, a);
        return LibCommandBuilder.named(getCommandName(tokens, a))
            .arguments(getArgumentText(entries, a))
            .description(String.join(" ", a.description()))
            .linter(a.linter().length == 0 ? this.mod.getDefaultLinter() : tryInstantiate(a.linter()[0]))
            .type(a.type())
            .side(a.side())
            .generate(createBranch(entries, cmd));
    }

    private static String getCommandName(List tokens, ModCommand a) {
        if (!a.name().isEmpty()) return a.name();
        if (!a.value().isEmpty()) return a.value();
        return tokens.get(0);
    }

    private static String getArgumentText(List entries, ModCommand a) {
        if (!a.arguments().isEmpty()) return a.arguments();
        final StringBuilder sb = new StringBuilder();
        for (final ParsedNode entry : entries) {
            if (sb.length() > 0) sb.append(' ');
            if (entry.arg.isLiteral()) {
                sb.append(entry.name);
                continue;
            }
            if (entry.optional) sb.append('[');
            sb.append('<');
            sb.append(entry.name);
            if (entry.isList) sb.append("...");
            sb.append('>');
            if (entry.optional) sb.append(']');
        }
        return sb.toString();
    }

    private CommandGenerator
            createBranch(List entries, CommandFunction fn) {

        return (builder, utl) -> {
            ArgumentBuilder nextArg = null;
            final MutableInt index = new MutableInt(entries.size() - 1);
            final Command cmd = utl.wrap(fn);
            boolean optional = true;

            while (index.getValue() >= 0) {
                final ArgumentBuilder argument;
                try {
                    argument = createArgument(entries, nextArg, cmd, index);
                } catch (final FormattedException e) {
                    throw new UncheckedFormattedException(e);
                }

                if (optional) {
                    argument.executes(cmd);
                    optional = false;
                }
                final ParsedNode entry = entries.get(index.getValue());
                if (entry.optional || (entry.isList && index.getValue() == entries.size() - 1)) {
                    optional = true;
                }
                nextArg = nextArg != null ? argument.then(nextArg) : argument;
                index.decrement();
            }
            if (optional || entries.isEmpty()) {
                builder.executes(cmd);
            }
            return entries.isEmpty() ? builder : builder.then(nextArg);
        };
    }

    private List createEntries(List tokens, ModCommand a) {
        final List entries = new ArrayList<>();
        if (a.name().isEmpty() && a.value().isEmpty()) {
            addEntriesFromMethod(entries, tokens, a);
        }
        for (final Node node : a.branch()) {
            final ArgumentDescriptor arg = createDescriptor(node);
            final String name = getArgumentName(node, arg.getType());
            entries.add(new ParsedNode(node, name, arg));
        }
        return entries;
    }

    private void addEntriesFromMethod(List entries, List tokens, ModCommand a) {
        for (int i = 1; i < tokens.size(); i++) {
            final String token = tokens.get(i).toLowerCase();
            for (final Node node : a.branch()) {
                if (node.name().equals(token) || node.value().equals(token)) {
                    return;
                }
            }
            entries.add(new ParsedNode(token));
        }
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    private ArgumentDescriptor createDescriptor(Node node) {
        if (node.type().length > 0) {
            return new ArgumentDescriptor<>(tryInstantiate((Class>)node.type()[0]));
        } else if (node.descriptor().length > 0) {
            return tryInstantiate(node.descriptor()[0]).get();
        } else if (node.intRange().length > 0) {
            final Node.IntRange range = node.intRange()[0];
            return new ArgumentDescriptor<>(IntegerArgumentType.integer(range.min(), range.max()));
        } else if (node.doubleRange().length > 0) {
            final Node.DoubleRange range = node.doubleRange()[0];
            return new ArgumentDescriptor<>(DoubleArgumentType.doubleArg(range.min(), range.max()));
        } else if (node.isBoolean()) {
            return new ArgumentDescriptor<>(BoolArgumentType.bool());
        } else if (node.stringValue().length > 0) {
            return new ArgumentDescriptor<>(createStringArgumentType(node));
        } else if (node.enumValue().length > 0) {
            return new ArgumentDescriptor<>(EnumArgument.of((Class) node.enumValue()[0]));
        } else if (node.registry().length > 0) {
            return new ArgumentDescriptor<>(RegistryArgument.getOrThrow(node.registry()[0]));
        } else {
            return ArgumentDescriptor.LITERAL;
        }
    }

    private ArgumentType createStringArgumentType(Node node) {
        final Node.StringValue value = node.stringValue()[0];
        if (value.value() == Node.StringValue.Type.GREEDY) {
            return StringArgumentType.greedyString();
        } else if (value.value() == Node.StringValue.Type.STRING) {
            return StringArgumentType.string();
        }
        return StringArgumentType.word();
    }

    @SuppressWarnings("unchecked")
    private ArgumentBuilder createArgument(
            List entries, ArgumentBuilder next,
            Command cmd, MutableInt index) throws FormattedException {

        final ParsedNode entry = entries.get(index.getValue());
        final ArgumentBuilder argument;
        final ArgumentDescriptor descriptor = entry.arg;
        final ArgumentType type = descriptor.getType();

        if (entry.isList) {
            argument = createList(entries, next, cmd, index.getValue());
        } else if (descriptor.isLiteral()) {
            argument = class_2170.method_9247(entry.name);
        } else {
            argument = class_2170.method_9244(entry.name, type);
        }
        if (descriptor.getSuggestions() != null) {
            ((RequiredArgumentBuilder) argument)
                .suggests(descriptor.getSuggestions());
        }
        return argument;
    }

    private String getArgumentName(Node node, ArgumentType type) {
        if (!node.name().isEmpty()) return node.name();
        if (!node.value().isEmpty()) return node.value();
        return type.getClass().getSimpleName();
    }

    private ArgumentBuilder createList(
            List entries, ArgumentBuilder next,
            Command cmd, int index) throws FormattedException {

        final ParsedNode entry = entries.get(index);
        final ListArgumentBuilder listBuilder =
            ListArgumentBuilder.create(entry.name, entry.arg.getType());

        if (index < entries.size() - 1) {
            final ParsedNode nextEntry = entries.get(index + 1);
            if (!nextEntry.arg.isLiteral()) {
                throw new InvalidListNodeException(entry.name);
            }
            return listBuilder.terminatedBy(next).build();
        } else {
            return listBuilder.executes(cmd).build();
        }
    }

    @SuppressWarnings("unchecked")
    private  T getValue(Method m) throws CommandClassEvaluationException {
        m.setAccessible(true);
        try {
            return (T) m.invoke(this.instance);
        } catch (final IllegalAccessException | InvocationTargetException ignored) {
            throw new CommandClassEvaluationException(COULD_NOT_INVOKE, m);
        }
    }

    private CommandFunction createConsumer(Object instance, Method m) {
        m.setAccessible(true);
        final Parameter[] params = removeImplicit(m.getParameters());
        if (params.length == 0) {
            return ctx -> m.invoke(instance);
        } else if (params.length == 1 && params[0].getType().isAssignableFrom(CommandContextWrapper.class)) {
            return ctx -> m.invoke(instance, ctx);
        }
        return ctx -> m.invoke(instance, getArgs(ctx, params));
    }

    private static Parameter[] removeImplicit(Parameter[] params) {
        if (params.length == 0) return params;
        if (params[0].isImplicit()) return ArrayUtils.subarray(params, 1, params.length);
        return params;
    }

    private Object[] getArgs(CommandContextWrapper ctx, Parameter[] params) throws CommandClassEvaluationException {
        final Object[] args = new Object[params.length];
        for (int i = 0; i < params.length; i++) {
            final Parameter param = params[i];
            final Class type = param.getType();
            final String name = getName(param);
            if (type.isAssignableFrom(CommandContextWrapper.class)) {
                args[i] = ctx;
            } else if (type.isAssignableFrom(Optional.class)) {
                args[i] = getOptional(ctx, name, getTypeArg(param));
            } else if (type.isAssignableFrom(List.class)) {
                args[i] = getList(ctx, name, getTypeArg(param));
            } else if (type.isArray() || param.isVarArgs()) {
                final Class arg = type.getComponentType();
                args[i] = toArray(arg, getList(ctx, name, arg));
            } else if (isNullable(param)) {
                args[i] = getOptional(ctx, name, type).orElse(null);
            } else {
                args[i] = get(ctx, name, type);
            }
        }
        return args;
    }

    private String getName(final Parameter param) throws CommandClassEvaluationException {
        if (!param.isNamePresent()) {
            final Method m = (Method) param.getDeclaringExecutable();
            throw new CommandClassEvaluationException(MISSING_COMPILE_ARG, m);
        }
        return param.getName();
    }

    private static Class getTypeArg(final Parameter param) {
        return (Class) ((ParameterizedType) param.getParameterizedType()).getActualTypeArguments()[0];
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    private static Object get(final CommandContextWrapper ctx, final String name, final Class type) {
        for (final Mapping mapping : AUTOMATIC_MAPPINGS) {
            if (type.isAssignableFrom(mapping.to)) {
                final Optional o = ctx.getOptional(name, mapping.from).map((Function) mapping.mapper);
                if (o.isPresent()) return o.get();
                break;
            }
        }
        return ctx.get(name, type);
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    private static Optional getOptional(final CommandContextWrapper ctx, final String name, final Class type) {
        for (final Mapping mapping : AUTOMATIC_MAPPINGS) {
            if (type.isAssignableFrom(mapping.to)) {
                final Optional o = ctx.getOptional(name, mapping.from).map((Function) mapping.mapper);
                if (o.isPresent()) return o;
                break;
            }
        }
        return ctx.getOptional(name, type);
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    private static List getList(final CommandContextWrapper ctx, final String name, final Class type) {
        for (final Mapping mapping : AUTOMATIC_MAPPINGS) {
            if (type.isAssignableFrom(mapping.to)) {
                if (ctx.getOptional(name + "0", mapping.from).isPresent()) {
                    return Shorthand.map((List) ctx.getList(name, mapping.from), mapping.mapper);
                }
                break;
            }
        }
        return ctx.getList(name, type);
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    private static Object toArray(final Class cmpType, final List list) {
        if (!cmpType.isPrimitive()) {
            return list.toArray();
        }
        if (cmpType.isAssignableFrom(int.class)) {
            return Ints.toArray((Collection) list);
        } else if (cmpType.isAssignableFrom(double.class)) {
            return Doubles.toArray((Collection) list);
        } else if (cmpType.isAssignableFrom(float.class)) {
            return Floats.toArray((Collection) list);
        } else if (cmpType.isAssignableFrom(boolean.class)) {
            return Booleans.toArray((Collection) list);
        } else if (cmpType.isAssignableFrom(byte.class)) {
            return Bytes.toArray((Collection) list);
        } else if (cmpType.isAssignableFrom(short.class)) {
            return Shorts.toArray((Collection) list);
        } else if (cmpType.isAssignableFrom(long.class)) {
            return Longs.toArray((Collection) list);
        }
        return list.toArray();
    }

    private static boolean isNullable(final Parameter param) {
        for (final Annotation a : param.getAnnotations()) {
            if (a.annotationType().getSimpleName().equalsIgnoreCase("nullable")) {
                return true;
            }
        }
        return false;
    }

    private static class ParsedNode {
        final ArgumentDescriptor arg;
        final String name;
        final boolean optional;
        final boolean isList;

        ParsedNode(Node node, String name, ArgumentDescriptor arg) {
            this.arg = arg;
            this.name = name;
            this.optional = node.optional();
            this.isList = node.intoList().useList();
        }

        ParsedNode(String name) {
            this.arg = ArgumentDescriptor.LITERAL;
            this.name = name;
            this.optional = false;
            this.isList = false;
        }
    }

    private static class Mapping {
        final Class from;
        final Class to;
        final Function mapper;

        Mapping(Class from, Class to, Function mapper) {
            this.from = from;
            this.to = to;
            this.mapper = mapper;
        }
    }

    private class CommandClassEvaluationException extends FormattedException {
        final Method method;

        CommandClassEvaluationException(String msg, Method method) {
            super(msg);
            this.method = method;
        }

        @Override
        public @NotNull String getCategory() {
            return COMMAND_CATEGORY;
        }

        @Override
        public @NotNull class_2561 getDisplayMessage() {
            return new class_2585(this.getFullMethod());
        }

        @Override
        public @Nullable class_2561 getTooltip() {
            return new class_2588(this.getMessage(), this.method.getName());
        }

        @Override
        public @NotNull class_2561 getTitleMessage() {
            return new class_2588(GENERIC_ERROR, this.getFullMethod());
        }

        private String getFullMethod() {
            return clazz.getSimpleName() + "#" + this.method.getName();
        }
    }

    private static class InvalidListNodeException extends FormattedException {
        private final String name;

        InvalidListNodeException(String name) {
            super(COMMAND_NODE);
            this.name = name;
        }

        @Override
        public @NotNull String getCategory() {
            return COMMAND_CATEGORY;
        }

        @Override
        public @NotNull class_2561 getDisplayMessage() {
            return new class_2588(DISPLAY_NODE, this.name);
        }

        @Override
        public @Nullable class_2561 getTooltip() {
            return new class_2588(LIST_NODE, this.name);
        }

        @Override
        public @NotNull class_2561 getTitleMessage() {
            return new class_2588(COMMAND_NODE, this.name);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy