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

net.forthecrown.grenadier.annotations.compiler.CommandCompiler Maven / Gradle / Ivy

There is a newer version: 1.3.3
Show newest version
package net.forthecrown.grenadier.annotations.compiler;

import com.mojang.brigadier.Command;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.ArgumentBuilder;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.tree.CommandNode;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.function.Consumer;
import java.util.function.Predicate;
import net.forthecrown.grenadier.CommandSource;
import net.forthecrown.grenadier.Completions;
import net.forthecrown.grenadier.GrenadierCommand;
import net.forthecrown.grenadier.GrenadierCommandNode;
import net.forthecrown.grenadier.Nodes;
import net.forthecrown.grenadier.annotations.ArgumentModifier;
import net.forthecrown.grenadier.annotations.TypeRegistry.TypeParser;
import net.forthecrown.grenadier.annotations.tree.AbstractCmdTree;
import net.forthecrown.grenadier.annotations.tree.ArgumentMapperTree.MemberMapper;
import net.forthecrown.grenadier.annotations.tree.ArgumentMapperTree.ResultMemberMapper;
import net.forthecrown.grenadier.annotations.tree.ArgumentMapperTree.VariableMapper;
import net.forthecrown.grenadier.annotations.tree.ArgumentTree;
import net.forthecrown.grenadier.annotations.tree.ArgumentTypeTree.TypeInfoTree;
import net.forthecrown.grenadier.annotations.tree.ArgumentTypeTree.VariableTypeReference;
import net.forthecrown.grenadier.annotations.tree.DescriptionTree;
import net.forthecrown.grenadier.annotations.tree.DescriptionTree.ArrayDescription;
import net.forthecrown.grenadier.annotations.tree.DescriptionTree.LiteralDescription;
import net.forthecrown.grenadier.annotations.tree.DescriptionTree.TranslatableDescription;
import net.forthecrown.grenadier.annotations.tree.DescriptionTree.VariableDescription;
import net.forthecrown.grenadier.annotations.tree.ExecutesTree.MemberExecutes;
import net.forthecrown.grenadier.annotations.tree.ExecutesTree.VariableExecutes;
import net.forthecrown.grenadier.annotations.tree.LiteralTree;
import net.forthecrown.grenadier.annotations.tree.Name;
import net.forthecrown.grenadier.annotations.tree.Name.DirectName;
import net.forthecrown.grenadier.annotations.tree.Name.FieldReferenceName;
import net.forthecrown.grenadier.annotations.tree.Name.VariableName;
import net.forthecrown.grenadier.annotations.tree.RequiresTree.ConstantRequires;
import net.forthecrown.grenadier.annotations.tree.RequiresTree.MemberRequires;
import net.forthecrown.grenadier.annotations.tree.RequiresTree.PermissionRequires;
import net.forthecrown.grenadier.annotations.tree.RequiresTree.VariableRequires;
import net.forthecrown.grenadier.annotations.tree.RootTree;
import net.forthecrown.grenadier.annotations.tree.SuggestsTree.MemberSuggestions;
import net.forthecrown.grenadier.annotations.tree.SuggestsTree.StringListSuggestions;
import net.forthecrown.grenadier.annotations.tree.SuggestsTree.VariableSuggestions;
import net.forthecrown.grenadier.annotations.tree.TreeVisitor;
import net.forthecrown.grenadier.annotations.util.Result;
import net.kyori.adventure.text.Component;
import org.bukkit.permissions.Permission;

public class CommandCompiler implements TreeVisitor {

  private static final String FAILED = "FAILED";

  public static final CommandCompiler COMPILER = new CommandCompiler();

  /* ------------------------------- NODES -------------------------------- */

  @Override
  public CommandNode visitLiteral(LiteralTree tree, CompileContext context) {
    Result nameResult = (Result) tree.getName().accept(this, context);

    nameResult.report(context.getErrors());

    String name = nameResult.orElse(FAILED);
    var literal = Nodes.literal(name);

    genericNodeVisit(tree, literal, context, name);

    return literal.build();
  }

  @Override
  public CommandNode visitArgument(ArgumentTree tree, CompileContext context) {
    Result typeResult = (Result)
        tree.getTypeInfo().accept(this, context);

    typeResult.report(context.getErrors());

    ArgumentType type = typeResult.isError()
        ? StringArgumentType.word()
        : typeResult.getValue();

    Result nameResult = (Result) tree.getName().accept(this, context);

    nameResult.report(context.getErrors());

    String name = nameResult.orElse(FAILED);

    if (!nameResult.isError()) {
      if (context.availableArguments.contains(name)) {
        context.getErrors().error(tree.getName().tokenStart(),
            "Duplicate argument name, %s already declared", name
        );
      }

      context.availableArguments.push(name);
    }

    var builder = Nodes.argument(name, type);

    if (tree.getSuggests() != null) {
      Result> providerResult
          = (Result>)
          tree.getSuggests().accept(this, context);

      providerResult.apply(context.errors, builder::suggests);
    }

    genericNodeVisit(tree, builder, context, name);

    if (!nameResult.isError()) {
      context.availableArguments.pop();
    }

    return builder.build();
  }

  @Override
  public GrenadierCommandNode visitRoot(RootTree tree, CompileContext context) {
    Result nameResult = (Result) tree.getName().accept(this, context);
    nameResult.report(context.errors);

    String name = nameResult.orElse(FAILED);
    GrenadierCommand builder = new GrenadierCommand(name);

    builder.withPlainTranslation(tree.isPlainTranslation());

    if (tree.getPermission() != null) {
      consumeName(tree.getPermission(), context, s -> {
        String permission = s.replace("{command}", name);
        builder.withPermission(permission);
        context.conditions.push(new PermissionPredicate(permission));
      });
    } else {
      builder.withPermission(context.defaultedPermission(name));
    }

    if (tree.getAliases() != null && !tree.getAliases().isEmpty()) {
      tree.getAliases().forEach(name1 -> {
        consumeName(name1, context, s -> builder.getAliases().add(s));
      });
    }

    genericNodeVisit(tree, builder, context, name);
    return builder.build();
  }

  private void genericNodeVisit(
      AbstractCmdTree tree,
      ArgumentBuilder builder,
      CompileContext context,
      String name
  ) {
    int pushed = compileMappers(tree, context, name);

    String argumentLabel;

    if (tree.getSyntaxLabel() == null) {
      argumentLabel = tree instanceof ArgumentTree
          ? "<%s>".formatted(name)
          : name;

    } else {
      Result argLabel = (Result) tree.getSyntaxLabel().accept(this, context);
      argLabel.report(context.errors);
      argumentLabel = argLabel.orElse(FAILED);
    }

    context.syntaxPrefixes.push(argumentLabel);

    if (tree.getDescription() != null) {
      consumeDescription(tree.getDescription(), context, component -> {
        if (builder instanceof GrenadierCommand gren) {
          if (tree.getExecutes() != null) {
            consumeSyntax(context, component);
          }

          gren.withDescription(component);
          return;
        }

        consumeSyntax(context, component);
      });
    }

    final boolean[] conditionPushed = new boolean[1];
    if (tree.getRequires() != null) {
      Result> predicateResult
          = (Result>)
          tree.getRequires().accept(this, context);

      predicateResult.apply(context.errors, predicate -> {
        conditionPushed[0] = true;
        context.conditions.push(predicate);
        builder.requires(predicate);
      });
    }

    if (tree.getExecutes() != null) {
      Result> cmdResult
          = (Result>)
          tree.getExecutes().accept(this, context);

      cmdResult.apply(context.errors, builder::executes);
    }

    tree.getChildren().forEach(child -> {
      CommandNode node = (CommandNode) child.accept(this, context);
      builder.then(node);
    });

    context.syntaxPrefixes.pop();

    if (conditionPushed[0]) {
      context.conditions.pop();
    }

    for (int i = 0; i < pushed; i++) {
      context.mappers.pop();
    }
  }

  private void consumeDescription(
      DescriptionTree tree,
      CompileContext context,
      Consumer consumer
  ) {
    Result descRes = (Result) tree.accept(this, context);
    descRes.apply(context.errors, consumer);
  }

  private void consumeSyntax(CompileContext context, Component component) {
    String label = context.syntaxPrefix();
    context.syntaxList.add(label, component, context.buildConditions());
  }

  @SuppressWarnings({"unchecked", "rawtypes"})
  private int compileMappers(
      AbstractCmdTree tree,
      CompileContext context,
      String argumentName
  ) {
    if (tree.getMappers() == null || tree.getMappers().isEmpty()) {
      return 0;
    }

    int pushed = 0;

    for (var m: tree.getMappers()) {
      Result modifierResult = (Result) m.accept(this, context);
      String modifierArgumentName;

      if (m.argumentName() == null) {
        modifierArgumentName = argumentName;
      } else {
        Result mapperNameResult = (Result) m.argumentName().accept(this, context);

        mapperNameResult.report(context.errors);
        modifierArgumentName = mapperNameResult.orElse(FAILED);

        if (!mapperNameResult.isError()) {
          boolean argumentExists = context.availableArguments.contains(modifierArgumentName);

          if (!argumentExists) {
            context.errors.warning(
                m.argumentName().tokenStart(),
                "Argument '%s' doesn't exist in current scope",
                modifierArgumentName
            );
          }
        }
      }

      modifierResult.report(context.errors);

      if (!modifierResult.isError()) {
        pushed++;
        context.mappers.push(new MapperEntry(modifierArgumentName, modifierResult.getValue()));
      }
    }

    return pushed;
  }

  private void consumeName(Name name, CompileContext context, Consumer consumer) {
    Result res = (Result) name.accept(this, context);
    res.apply(context.errors, consumer);
  }

  /* ---------------------------- SUGGESTIONS ----------------------------- */

  @Override
  public Result visitStringSuggestions(
      StringListSuggestions tree,
      CompileContext context
  ) {
    String[] suggestions = tree.suggestions();

    return Result.success((context1, builder) -> {
      return Completions.suggest(builder, suggestions);
    });
  }

  @Override
  public Result visitMemberSuggestions(
      MemberSuggestions tree,
      CompileContext context
  ) {
    return CompiledSuggester.compile(tree, context);
  }

  @Override
  public Result visitVariableSuggests(
      VariableSuggestions tree,
      CompileContext context
  ) {
    return context.getVariable(tree, SuggestionProvider.class);
  }

  /* ---------------------------- REQUIREMENTS ---------------------------- */

  @Override
  public Result> visitPermissionRequirement(
      PermissionRequires tree,
      CompileContext context
  ) {
    Result permissionResult
        = (Result) tree.name().accept(this, context);

    return permissionResult.map(PermissionPredicate::new);
  }

  @Override
  public Result visitMemberRequirement(
      MemberRequires tree,
      CompileContext context
  ) {
    return CompiledRequires.compile(tree, context);
  }

  @Override
  public Result visitConstantRequires(ConstantRequires tree,
                                                 CompileContext context
  ) {
    boolean value = tree.value();
    return Result.success(o -> value);
  }

  @Override
  public Result visitVariableRequires(
      VariableRequires tree,
      CompileContext context
  ) {
    return context.getVariable(tree, Predicate.class);
  }

  /* ------------------------ ARGUMENT TYPE INFOS ------------------------- */

  @Override
  public Result visitArgumentTypeTree(TypeInfoTree tree,
                                                    CompileContext context
  ) {
    var registry = context.getTypeRegistry();
    TypeParser parser = registry.getParser(tree.name());

    if (parser == null) {
      return Result.fail(tree.tokenStart(),
          "Unknown argument type '%s'", tree.name()
      );
    }

    return (Result) parser.parse(tree, context);
  }

  @Override
  public Result visitVariableArgumentType(VariableTypeReference tree,
                                                        CompileContext context
  ) {
    return context.getVariable(tree, ArgumentType.class);
  }

  /* ----------------------------- EXECUTIONS ----------------------------- */

  @Override
  public Result visitVariableExecutes(VariableExecutes tree,
                                               CompileContext context
  ) {
    return context.getVariable(tree, Command.class);
  }

  @Override
  public Result visitMemberExecutes(MemberExecutes tree,
                                             CompileContext context
  ) {
    return CompiledExecutes.compile(tree, context);
  }

  /* ------------------------------- NAMES -------------------------------- */

  @Override
  public Result visitVariableName(VariableName tree, CompileContext context) {
    return context.getVariable(tree, String.class)
        .or(() -> context.getVariable(tree, Permission.class).map(Permission::getName));
  }

  @Override
  public Result visitDirectName(DirectName tree, CompileContext context) {
    return Result.success(tree.value());
  }

  @Override
  public Result visitFieldName(FieldReferenceName tree, CompileContext context) {
    int start = tree.tokenStart();
    Class cmdClass = context.getCommandClass().getClass();
    Field nameField;

    try {
      nameField = cmdClass.getDeclaredField(tree.fieldName());
    } catch (ReflectiveOperationException exc) {
      return Result.fail(start,
          "Reflection error while accessing field '%s': %s",
          tree.fieldName(), exc.getMessage()
      );
    }

    if (!Modifier.isFinal(nameField.getModifiers())) {
      context.errors.warning(start,
          "Field '%s' in '%s' is not final! Changes to "
              + "this field will not be reflected in the command tree",

          nameField.getName(), nameField.getDeclaringClass()
      );
    }

    if (CharSequence.class.isAssignableFrom(nameField.getDeclaringClass())) {
      return Result.fail(start,
          "Cannot get name from field '%s' in %s. "
              + "Field's type is not an inheritor of java.lang.CharSequence",

          nameField.getName(), nameField.getDeclaringClass()
      );
    }

    try {
      var overriden = nameField.isAccessible();
      nameField.setAccessible(true);

      Object o = nameField.get(context.getCommandClass());

      nameField.setAccessible(overriden);

      CharSequence sequence = (CharSequence) o;

      if (sequence == null) {
        return Result.fail(start,
            "Field '%s' in %s returned null! Cannot get name",
            nameField.getName(), nameField.getDeclaringClass()
        );
      }

      return Result.success(sequence.toString());
    } catch (IllegalAccessException e) {
      return Result.fail(start,
          "Illegal access to field '%s' in %s. "
              + "This error shouldn't happen",

          nameField.getName(), nameField.getDeclaringClass()
      );
    }
  }

  /* --------------------------- RESULT MAPPERS --------------------------- */

  @Override
  public Result visitVariableMapper(VariableMapper tree, CompileContext context) {
    return context.getVariable(tree, ArgumentModifier.class);
  }

  @Override
  public Result visitMemberMapper(MemberMapper tree, CompileContext context) {
    // TODO perform validation of reference before returning success
    return Result.success(
        new CompiledArgumentMapper(context.getCommandClass(), tree.ref())
    );
  }

  @Override
  public Object visitResultMemberMapper(ResultMemberMapper tree, CompileContext context) {
    // TODO perform validation of reference before returning success
    return Result.success(
        new CompiledArgumentMapper(null, tree.ref())
    );
  }

  /* ---------------------------- DESCRIPTION ----------------------------- */

  @Override
  public Result visitLiteralDescription(
      LiteralDescription tree,
      CompileContext context
  ) {
    return Result.success(Component.text(tree.value()));
  }

  @Override
  public Result visitVariableDescription(
      VariableDescription tree,
      CompileContext context
  ) {
    return context.getVariable(tree, Component.class);
  }

  @Override
  public Result visitTranslatableDescription(
      TranslatableDescription tree,
      CompileContext context
  ) {
    return Result.success(
        Component.translatable(tree.translationKey())
    );
  }

  @Override
  public Result visitArrayDescription(
      ArrayDescription tree,
      CompileContext context
  ) {
    if (tree.elements().length < 1) {
      return Result.fail(tree.tokenStart(), "Empty description");
    }

    var builder = Component.text();
    final boolean[] addedAny = { false };

    for (DescriptionTree element : tree.elements()) {
      Result res
          = (Result) element.accept(this, context);

      res.apply(context.errors, component -> {
        if (addedAny[0]) {
          builder.append(Component.newline());
        }

        builder.append(component);
        addedAny[0] = true;
      });
    }

    if (addedAny[0]) {
      return Result.success(builder.build());
    } else {
      return Result.fail(tree.tokenStart(),
          "Failed to read any elements for description"
      );
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy