diff --git a/src/client/java/works/nuty/calcite/mixin/client/SuggestionsBuilderFields.java b/src/client/java/works/nuty/calcite/mixin/client/SuggestionsBuilderFields.java new file mode 100644 index 0000000..caa059c --- /dev/null +++ b/src/client/java/works/nuty/calcite/mixin/client/SuggestionsBuilderFields.java @@ -0,0 +1,14 @@ +package works.nuty.calcite.mixin.client; + +import com.mojang.brigadier.suggestion.Suggestion; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.List; + +@Mixin(SuggestionsBuilder.class) +public interface SuggestionsBuilderFields { + @Accessor + List getResult(); +} diff --git a/src/client/java/works/nuty/calcite/mixin/client/argument/NbtCompoundArgumentTypeMixin.java b/src/client/java/works/nuty/calcite/mixin/client/argument/NbtCompoundArgumentTypeMixin.java new file mode 100644 index 0000000..0ae85fc --- /dev/null +++ b/src/client/java/works/nuty/calcite/mixin/client/argument/NbtCompoundArgumentTypeMixin.java @@ -0,0 +1,46 @@ +package works.nuty.calcite.mixin.client.argument; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import com.mojang.brigadier.tree.LiteralCommandNode; +import net.minecraft.command.CommandSource; +import net.minecraft.command.argument.NbtCompoundArgumentType; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.registry.entry.RegistryEntry; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import works.nuty.calcite.parser.EntityParser; + +import java.util.concurrent.CompletableFuture; + +@Mixin(NbtCompoundArgumentType.class) +public abstract class NbtCompoundArgumentTypeMixin implements ArgumentType { + public CompletableFuture listSuggestions(CommandContext context, SuggestionsBuilder builder) { + if (((LiteralCommandNode) context.getLastChild().getNodes().get(0).getNode()).getLiteral().equals("summon")) { + return listEntityStringNbtSuggestions(context, builder); + } else { + return Suggestions.empty(); + } + } + + @Unique + private CompletableFuture listEntityStringNbtSuggestions(CommandContext context, SuggestionsBuilder builder) { + String entityRegistryId = context.getLastChild().getArgument("entity", RegistryEntry.Reference.class).getIdAsString(); + Object object = context.getSource(); + if (object instanceof CommandSource) { + StringReader reader = new StringReader(builder.getInput()); + reader.setCursor(builder.getStart()); + EntityParser parser = new EntityParser(reader, entityRegistryId); + try { + parser.parse(); + } catch (CommandSyntaxException ignored) { + } + return parser.fillSuggestions(builder); + } + return Suggestions.empty(); + } +} diff --git a/src/client/java/works/nuty/calcite/parser/DefaultParser.java b/src/client/java/works/nuty/calcite/parser/DefaultParser.java new file mode 100644 index 0000000..d21924d --- /dev/null +++ b/src/client/java/works/nuty/calcite/parser/DefaultParser.java @@ -0,0 +1,34 @@ +package works.nuty.calcite.parser; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +public abstract class DefaultParser implements Parser { + private static final Function> SUGGEST_NOTHING = SuggestionsBuilder::buildFuture; + private final StringReader reader; + private Function> suggestions = SUGGEST_NOTHING; + + public DefaultParser(StringReader reader) { + this.reader = reader; + } + + public StringReader reader() { + return this.reader; + } + + public Function> getSuggestions() { + return suggestions; + } + + public void suggest(Function> suggestions) { + this.suggestions = suggestions; + } + + public void suggestNothing() { + this.suggestions = SUGGEST_NOTHING; + } +} diff --git a/src/client/java/works/nuty/calcite/parser/EntityParser.java b/src/client/java/works/nuty/calcite/parser/EntityParser.java new file mode 100644 index 0000000..b27d6cb --- /dev/null +++ b/src/client/java/works/nuty/calcite/parser/EntityParser.java @@ -0,0 +1,260 @@ +package works.nuty.calcite.parser; + +import com.google.common.collect.ImmutableMap; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.entity.*; +import net.minecraft.entity.boss.WitherEntity; +import net.minecraft.entity.boss.dragon.EnderDragonEntity; +import net.minecraft.entity.decoration.*; +import net.minecraft.entity.decoration.painting.PaintingEntity; +import net.minecraft.entity.mob.*; +import net.minecraft.entity.passive.*; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.projectile.*; +import net.minecraft.entity.projectile.thrown.*; +import net.minecraft.entity.vehicle.*; +import net.minecraft.text.Text; +import works.nuty.calcite.parser.options.EntityOptions; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public class EntityParser extends DefaultParser { + public static final SimpleCommandExceptionType EXPECTED_KEY = new SimpleCommandExceptionType(Text.translatable("argument.nbt.expected.key")); + public static final SimpleCommandExceptionType EXPECTED_VALUE = new SimpleCommandExceptionType(Text.translatable("argument.nbt.expected.value")); + private final static Map> ENTITY_REGISTRY_ID_2_CLASS = ImmutableMap.>builder() + .put("minecraft:allay", AllayEntity.class) + .put("minecraft:area_effect_cloud", AreaEffectCloudEntity.class) + .put("minecraft:armadillo", ArmadilloEntity.class) + .put("minecraft:armor_stand", ArmorStandEntity.class) + .put("minecraft:arrow", ArrowEntity.class) + .put("minecraft:axolotl", AxolotlEntity.class) + .put("minecraft:bat", BatEntity.class) + .put("minecraft:bee", BeeEntity.class) + .put("minecraft:blaze", BlazeEntity.class) + .put("minecraft:block_display", DisplayEntity.BlockDisplayEntity.class) + .put("minecraft:boat", BoatEntity.class) + .put("minecraft:bogged", BoggedEntity.class) + .put("minecraft:breeze", BreezeEntity.class) + .put("minecraft:breeze_wind_charge", BreezeWindChargeEntity.class) + .put("minecraft:camel", CamelEntity.class) + .put("minecraft:cat", CatEntity.class) + .put("minecraft:cave_spider", CaveSpiderEntity.class) + .put("minecraft:chest_boat", ChestBoatEntity.class) + .put("minecraft:chest_minecart", ChestMinecartEntity.class) + .put("minecraft:chicken", ChickenEntity.class) + .put("minecraft:cod", CodEntity.class) + .put("minecraft:command_block_minecart", CommandBlockMinecartEntity.class) + .put("minecraft:cow", CowEntity.class) + .put("minecraft:creeper", CreeperEntity.class) + .put("minecraft:dolphin", DolphinEntity.class) + .put("minecraft:donkey", DonkeyEntity.class) + .put("minecraft:dragon_fireball", DragonFireballEntity.class) + .put("minecraft:drowned", DrownedEntity.class) + .put("minecraft:egg", EggEntity.class) + .put("minecraft:elder_guardian", ElderGuardianEntity.class) + .put("minecraft:end_crystal", EndCrystalEntity.class) + .put("minecraft:ender_dragon", EnderDragonEntity.class) + .put("minecraft:ender_pearl", EnderPearlEntity.class) + .put("minecraft:enderman", EndermanEntity.class) + .put("minecraft:endermite", EndermiteEntity.class) + .put("minecraft:evoker", EvokerEntity.class) + .put("minecraft:evoker_fangs", EvokerFangsEntity.class) + .put("minecraft:experience_bottle", ExperienceBottleEntity.class) + .put("minecraft:experience_orb", ExperienceOrbEntity.class) + .put("minecraft:eye_of_ender", EyeOfEnderEntity.class) + .put("minecraft:falling_block", FallingBlockEntity.class) + .put("minecraft:firework_rocket", FireworkRocketEntity.class) + .put("minecraft:fox", FoxEntity.class) + .put("minecraft:frog", FrogEntity.class) + .put("minecraft:furnace_minecart", FurnaceMinecartEntity.class) + .put("minecraft:ghast", GhastEntity.class) + .put("minecraft:giant", GiantEntity.class) + .put("minecraft:glow_item_frame", GlowItemFrameEntity.class) + .put("minecraft:glow_squid", GlowSquidEntity.class) + .put("minecraft:goat", GoatEntity.class) + .put("minecraft:guardian", GuardianEntity.class) + .put("minecraft:hoglin", HoglinEntity.class) + .put("minecraft:hopper_minecart", HopperMinecartEntity.class) + .put("minecraft:horse", HorseEntity.class) + .put("minecraft:husk", HuskEntity.class) + .put("minecraft:illusioner", IllusionerEntity.class) + .put("minecraft:interaction", InteractionEntity.class) + .put("minecraft:iron_golem", IronGolemEntity.class) + .put("minecraft:item", ItemEntity.class) + .put("minecraft:item_display", DisplayEntity.ItemDisplayEntity.class) + .put("minecraft:item_frame", ItemFrameEntity.class) + .put("minecraft:fireball", FireballEntity.class) + .put("minecraft:leash_knot", LeashKnotEntity.class) + .put("minecraft:lightning_bolt", LightningEntity.class) + .put("minecraft:llama", LlamaEntity.class) + .put("minecraft:llama_spit", LlamaSpitEntity.class) + .put("minecraft:magma_cube", MagmaCubeEntity.class) + .put("minecraft:marker", MarkerEntity.class) + .put("minecraft:minecart", MinecartEntity.class) + .put("minecraft:mooshroom", MooshroomEntity.class) + .put("minecraft:mule", MuleEntity.class) + .put("minecraft:ocelot", OcelotEntity.class) + .put("minecraft:painting", PaintingEntity.class) + .put("minecraft:panda", PandaEntity.class) + .put("minecraft:parrot", ParrotEntity.class) + .put("minecraft:phantom", PhantomEntity.class) + .put("minecraft:pig", PigEntity.class) + .put("minecraft:piglin", PiglinEntity.class) + .put("minecraft:piglin_brute", PiglinBruteEntity.class) + .put("minecraft:pillager", PillagerEntity.class) + .put("minecraft:polar_bear", PolarBearEntity.class) + .put("minecraft:potion", PotionEntity.class) + .put("minecraft:pufferfish", PufferfishEntity.class) + .put("minecraft:rabbit", RabbitEntity.class) + .put("minecraft:ravager", RavagerEntity.class) + .put("minecraft:salmon", SalmonEntity.class) + .put("minecraft:sheep", SheepEntity.class) + .put("minecraft:shulker", ShulkerEntity.class) + .put("minecraft:shulker_bullet", ShulkerBulletEntity.class) + .put("minecraft:silverfish", SilverfishEntity.class) + .put("minecraft:skeleton", SkeletonEntity.class) + .put("minecraft:skeleton_horse", SkeletonHorseEntity.class) + .put("minecraft:slime", SlimeEntity.class) + .put("minecraft:small_fireball", SmallFireballEntity.class) + .put("minecraft:sniffer", SnifferEntity.class) + .put("minecraft:snow_golem", SnowGolemEntity.class) + .put("minecraft:snowball", SnowballEntity.class) + .put("minecraft:spawner_minecart", SpawnerMinecartEntity.class) + .put("minecraft:spectral_arrow", SpectralArrowEntity.class) + .put("minecraft:spider", SpiderEntity.class) + .put("minecraft:squid", SquidEntity.class) + .put("minecraft:stray", StrayEntity.class) + .put("minecraft:strider", StriderEntity.class) + .put("minecraft:tadpole", TadpoleEntity.class) + .put("minecraft:text_display", DisplayEntity.TextDisplayEntity.class) + .put("minecraft:tnt", TntEntity.class) + .put("minecraft:tnt_minecart", TntMinecartEntity.class) + .put("minecraft:trader_llama", TraderLlamaEntity.class) + .put("minecraft:trident", TridentEntity.class) + .put("minecraft:tropical_fish", TropicalFishEntity.class) + .put("minecraft:turtle", TurtleEntity.class) + .put("minecraft:vex", VexEntity.class) + .put("minecraft:villager", VillagerEntity.class) + .put("minecraft:vindicator", VindicatorEntity.class) + .put("minecraft:wandering_trader", WanderingTraderEntity.class) + .put("minecraft:warden", WardenEntity.class) + .put("minecraft:wind_charge", WindChargeEntity.class) + .put("minecraft:witch", WitchEntity.class) + .put("minecraft:wither", WitherEntity.class) + .put("minecraft:wither_skeleton", WitherSkeletonEntity.class) + .put("minecraft:wither_skull", WitherSkullEntity.class) + .put("minecraft:wolf", WolfEntity.class) + .put("minecraft:zoglin", ZoglinEntity.class) + .put("minecraft:zombie", ZombieEntity.class) + .put("minecraft:zombie_horse", ZombieHorseEntity.class) + .put("minecraft:zombie_villager", ZombieVillagerEntity.class) + .put("minecraft:zombified_piglin", ZombifiedPiglinEntity.class) + .put("minecraft:player", PlayerEntity.class) + .put("minecraft:fishing_bobber", FishingBobberEntity.class) + .build(); + private Class entityClass; + + public EntityParser(StringReader reader) { + this(reader, ""); + this.entityClass = Entity.class; + } + + public EntityParser(StringReader reader, String entityRegistryId) { + super(reader); + this.entityClass = getEntityClass(entityRegistryId); + } + + Class getEntityClass(String registryId) { + String prefixed = registryId.startsWith("minecraft:") ? registryId : ("minecraft:" + registryId); + return ENTITY_REGISTRY_ID_2_CLASS.getOrDefault(prefixed, Entity.class); + } + + public void parse() throws CommandSyntaxException { + this.suggest(this::suggestOpenCompound); + this.reader().expect('{'); + this.suggest(this::suggestOptionKey); + this.reader().skipWhitespace(); + while (this.reader().canRead() && this.reader().peek() != '}') { + int cursor = this.reader().getCursor(); + String key = this.reader().readString(); + EntityOptions.ValueHandler handler = EntityOptions.getValueHandler(this, key, this.reader().getCursor()); + + if (key.isEmpty() || this.hasPotentialOptionKey(key)) { + this.reader().setCursor(cursor); + throw EXPECTED_KEY.createWithContext(this.reader()); + } + this.reader().skipWhitespace(); + cursor = this.reader().getCursor(); + + if (!this.reader().canRead() || this.reader().peek() != ':') { + this.reader().setCursor(cursor); + this.suggest(this::suggestColon); + throw EXPECTED_VALUE.createWithContext(this.reader()); + } + this.reader().skip(); + this.reader().skipWhitespace(); + + this.suggestNothing(); + // todo handler should be non-null + if (handler != null) handler.handle(this); + this.reader().skipWhitespace(); + cursor = this.reader().getCursor(); + + this.suggest(this::suggestOptionsNextOrClose); + if (!this.reader().canRead()) continue; + if (this.reader().peek() == ',') { + this.reader().skip(); + this.reader().skipWhitespace(); + this.suggest(this::suggestOptionKey); + continue; + } + if (this.reader().peek() == ']') break; + this.reader().setCursor(cursor); + throw EXPECTED_KEY.createWithContext(this.reader()); + } + this.reader().expect('}'); + this.suggestNothing(); + } + + private CompletableFuture suggestOpenCompound(SuggestionsBuilder builder) { + builder.suggest("{"); + return builder.buildFuture(); + } + + private CompletableFuture suggestOptionsNextOrClose(SuggestionsBuilder builder) { + builder.suggest(","); + builder.suggest("}"); + return builder.buildFuture(); + } + + private CompletableFuture suggestOptionKey(SuggestionsBuilder builder) { + EntityOptions.suggestKeys(this, builder); + return builder.buildFuture(); + } + + private boolean hasPotentialOptionKey(String key) { + return EntityOptions.isPotentialKey(this, key); + } + + private CompletableFuture suggestColon(SuggestionsBuilder builder) { + builder.suggest(":"); + return builder.buildFuture(); + } + + public CompletableFuture fillSuggestions(SuggestionsBuilder builder) { + return super.getSuggestions().apply(builder.createOffset(this.reader().getCursor())); + } + + public Class getEntityClass() { + return entityClass; + } + + public void setEntityClass(Class entityClass) { + this.entityClass = entityClass; + } +} diff --git a/src/client/java/works/nuty/calcite/parser/ListParser.java b/src/client/java/works/nuty/calcite/parser/ListParser.java new file mode 100644 index 0000000..6ca0d32 --- /dev/null +++ b/src/client/java/works/nuty/calcite/parser/ListParser.java @@ -0,0 +1,75 @@ +package works.nuty.calcite.parser; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.predicate.NumberRange; + +import java.util.concurrent.CompletableFuture; + +public class ListParser extends DefaultParser { + private final DefaultParser parentParser; + private final DefaultParser elementParser; + private final NumberRange.IntRange sizeRange; + + public ListParser(DefaultParser parentParser, DefaultParser elementParser, NumberRange.IntRange sizeRange) { + super(parentParser.reader()); + this.parentParser = parentParser; + this.elementParser = elementParser; + this.sizeRange = sizeRange; + } + + private static CompletableFuture suggestListOpen(SuggestionsBuilder builder) { + builder.suggest("["); + return builder.buildFuture(); + } + + private static CompletableFuture suggestNext(SuggestionsBuilder builder) { + builder.suggest(","); + return builder.buildFuture(); + } + + private static CompletableFuture suggestListCloseOrNext(SuggestionsBuilder builder) { + builder.suggest(","); + builder.suggest("]"); + return builder.buildFuture(); + } + + private static CompletableFuture suggestListClose(SuggestionsBuilder builder) { + builder.suggest("]"); + return builder.buildFuture(); + } + + public void parse() throws CommandSyntaxException { + parentParser.suggest(ListParser::suggestListOpen); + reader().expect('['); + reader().skipWhitespace(); + int i = 0; + do { + ++i; + this.suggestNothing(); + elementParser.parse(); + this.reader().skipWhitespace(); + + if (i < sizeRange.min().orElse(0)) { + parentParser.suggest(ListParser::suggestNext); + reader().expect(','); + } else if (i < sizeRange.max().orElse(Integer.MAX_VALUE)) { + parentParser.suggest(ListParser::suggestListCloseOrNext); + if (!reader().canRead()) + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerExpectedSymbol().createWithContext(reader(), ','); + if (reader().peek() == ',') { + reader().skip(); + } else { + reader().expect(']'); + return; + } + } else { + parentParser.suggest(ListParser::suggestListClose); + reader().expect(']'); + return; + } + reader().skipWhitespace(); + } while (true); + } +} diff --git a/src/client/java/works/nuty/calcite/parser/Parser.java b/src/client/java/works/nuty/calcite/parser/Parser.java new file mode 100644 index 0000000..57c48ea --- /dev/null +++ b/src/client/java/works/nuty/calcite/parser/Parser.java @@ -0,0 +1,7 @@ +package works.nuty.calcite.parser; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; + +public interface Parser { + void parse() throws CommandSyntaxException; +} diff --git a/src/client/java/works/nuty/calcite/parser/UUIDParser.java b/src/client/java/works/nuty/calcite/parser/UUIDParser.java new file mode 100644 index 0000000..70d61cb --- /dev/null +++ b/src/client/java/works/nuty/calcite/parser/UUIDParser.java @@ -0,0 +1,38 @@ +package works.nuty.calcite.parser; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.predicate.NumberRange; +import net.minecraft.util.math.random.Random; +import works.nuty.calcite.CalciteModClient; +import works.nuty.calcite.parser.array.IntArrayParser; +import works.nuty.calcite.parser.primitive.IntParser; + +import java.util.concurrent.CompletableFuture; + +public class UUIDParser extends IntArrayParser { + private final DefaultParser parentParser; + + public UUIDParser(DefaultParser parentParser) { + super(parentParser, new IntParser(parentParser), NumberRange.IntRange.exactly(4)); + this.parentParser = parentParser; + } + + public static CompletableFuture suggestRandomUUID(SuggestionsBuilder builder) { + Random random = Random.create(); + builder.suggest("[I; " + random.nextInt() + ", " + random.nextInt() + ", " + random.nextInt() + ", " + random.nextInt() + "]"); + return builder.buildFuture(); + } + + @Override + public void parse() throws CommandSyntaxException { + if (reader().canRead() && reader().peek() == '[') { + CalciteModClient.LOGGER.info("calling super"); + super.parse(); + } else { + parentParser.suggest(UUIDParser::suggestRandomUUID); + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerExpectedSymbol().createWithContext(reader(), "[I;"); + } + } +} diff --git a/src/client/java/works/nuty/calcite/parser/array/ArrayParser.java b/src/client/java/works/nuty/calcite/parser/array/ArrayParser.java new file mode 100644 index 0000000..26d7084 --- /dev/null +++ b/src/client/java/works/nuty/calcite/parser/array/ArrayParser.java @@ -0,0 +1,89 @@ +package works.nuty.calcite.parser.array; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.predicate.NumberRange; +import works.nuty.calcite.parser.DefaultParser; + +import java.util.concurrent.CompletableFuture; + +public class ArrayParser extends DefaultParser { + private final DefaultParser parentParser; + private final DefaultParser elementParser; + private final NumberRange.IntRange sizeRange; + private final char prefix; + + public ArrayParser(DefaultParser parentParser, DefaultParser elementParser, NumberRange.IntRange sizeRange, char prefix) { + super(parentParser.reader()); + this.parentParser = parentParser; + this.elementParser = elementParser; + this.sizeRange = sizeRange; + this.prefix = prefix; + } + + private static CompletableFuture suggestNext(SuggestionsBuilder builder) { + builder.suggest(","); + return builder.buildFuture(); + } + + private static CompletableFuture suggestListCloseOrNext(SuggestionsBuilder builder) { + builder.suggest(","); + builder.suggest("]"); + return builder.buildFuture(); + } + + private static CompletableFuture suggestListClose(SuggestionsBuilder builder) { + builder.suggest("]"); + return builder.buildFuture(); + } + + public void parse() throws CommandSyntaxException { + parentParser.suggest(this::suggestArrayOpen); + int start = reader().getCursor(); + if (!reader().canRead(3)) { + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerExpectedSymbol().createWithContext(reader(), "[" + prefix + ";"); + } + try { + reader().expect('['); + reader().expect(prefix); + reader().expect(';'); + } catch (CommandSyntaxException e) { + reader().setCursor(start); + throw e; + } + reader().skipWhitespace(); + int i = 0; + do { + ++i; + this.suggestNothing(); + elementParser.parse(); + this.reader().skipWhitespace(); + + if (i < sizeRange.min().orElse(0)) { + parentParser.suggest(ArrayParser::suggestNext); + reader().expect(','); + } else if (i < sizeRange.max().orElse(Integer.MAX_VALUE)) { + parentParser.suggest(ArrayParser::suggestListCloseOrNext); + if (!reader().canRead()) + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerExpectedSymbol().createWithContext(reader(), ','); + if (reader().peek() == ',') { + reader().skip(); + } else { + reader().expect(']'); + return; + } + } else { + parentParser.suggest(ArrayParser::suggestListClose); + reader().expect(']'); + return; + } + reader().skipWhitespace(); + } while (true); + } + + private CompletableFuture suggestArrayOpen(SuggestionsBuilder builder) { + builder.suggest("[" + prefix + ";"); + return builder.buildFuture(); + } +} diff --git a/src/client/java/works/nuty/calcite/parser/array/ByteArrayParser.java b/src/client/java/works/nuty/calcite/parser/array/ByteArrayParser.java new file mode 100644 index 0000000..184bf3d --- /dev/null +++ b/src/client/java/works/nuty/calcite/parser/array/ByteArrayParser.java @@ -0,0 +1,10 @@ +package works.nuty.calcite.parser.array; + +import net.minecraft.predicate.NumberRange; +import works.nuty.calcite.parser.DefaultParser; + +public class ByteArrayParser extends ArrayParser { + public ByteArrayParser(DefaultParser parentParser, DefaultParser elementParser, NumberRange.IntRange sizeRange) { + super(parentParser, elementParser, sizeRange, 'B'); + } +} diff --git a/src/client/java/works/nuty/calcite/parser/array/IntArrayParser.java b/src/client/java/works/nuty/calcite/parser/array/IntArrayParser.java new file mode 100644 index 0000000..d4c64a0 --- /dev/null +++ b/src/client/java/works/nuty/calcite/parser/array/IntArrayParser.java @@ -0,0 +1,10 @@ +package works.nuty.calcite.parser.array; + +import net.minecraft.predicate.NumberRange; +import works.nuty.calcite.parser.DefaultParser; + +public class IntArrayParser extends ArrayParser { + public IntArrayParser(DefaultParser parentParser, DefaultParser elementParser, NumberRange.IntRange sizeRange) { + super(parentParser, elementParser, sizeRange, 'I'); + } +} diff --git a/src/client/java/works/nuty/calcite/parser/array/LongArrayParser.java b/src/client/java/works/nuty/calcite/parser/array/LongArrayParser.java new file mode 100644 index 0000000..2aeb344 --- /dev/null +++ b/src/client/java/works/nuty/calcite/parser/array/LongArrayParser.java @@ -0,0 +1,10 @@ +package works.nuty.calcite.parser.array; + +import net.minecraft.predicate.NumberRange; +import works.nuty.calcite.parser.DefaultParser; + +public class LongArrayParser extends ArrayParser { + public LongArrayParser(DefaultParser parentParser, DefaultParser elementParser, NumberRange.IntRange sizeRange) { + super(parentParser, elementParser, sizeRange, 'L'); + } +} diff --git a/src/client/java/works/nuty/calcite/parser/options/EntityOptions.java b/src/client/java/works/nuty/calcite/parser/options/EntityOptions.java new file mode 100644 index 0000000..047b831 --- /dev/null +++ b/src/client/java/works/nuty/calcite/parser/options/EntityOptions.java @@ -0,0 +1,108 @@ +package works.nuty.calcite.parser.options; + +import com.google.common.collect.ImmutableMap; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.entity.Entity; +import net.minecraft.predicate.NumberRange; +import works.nuty.calcite.parser.DefaultParser; +import works.nuty.calcite.parser.EntityParser; +import works.nuty.calcite.parser.ListParser; +import works.nuty.calcite.parser.UUIDParser; +import works.nuty.calcite.parser.primitive.*; + +import java.util.Locale; +import java.util.Map; +import java.util.function.Predicate; + +public class EntityOptions { + private static final Map OPTIONS = ImmutableMap.builder() + .put("Pos", new Option(Entity.class, parser -> new ListParser(parser, new DoubleParser(parser), NumberRange.IntRange.exactly(3)).parse())) + .put("Motion", new Option(Entity.class, parser -> new ListParser(parser, new DoubleParser(parser), NumberRange.IntRange.exactly(3)).parse())) + .put("Rotation", new Option(Entity.class, parser -> new ListParser(parser, new FloatParser(parser), NumberRange.IntRange.exactly(2)).parse())) + .put("FallDistance", new Option(Entity.class, parser -> new FloatParser(parser).parse())) + .put("Fire", new Option(Entity.class, parser -> new ShortParser(parser).parse())) + .put("Air", new Option(Entity.class, parser -> new ShortParser(parser).parse())) + .put("OnGround", new Option(Entity.class, parser -> new BooleanParser(parser).parse())) + .put("Invulnerable", new Option(Entity.class, parser -> new BooleanParser(parser).parse())) + .put("PortalCooldown", new Option(Entity.class, parser -> new IntParser(parser).parse())) + .put("UUID", new Option(Entity.class, parser -> new UUIDParser(parser).parse())) + .put("CustomName", new Option(Entity.class, parser -> new StringParser(parser).parse())) + .put("CustomNameVisible", new Option(Entity.class, parser -> new BooleanParser(parser).parse())) + .put("Silent", new Option(Entity.class, parser -> new BooleanParser(parser).parse())) + .put("NoGravity", new Option(Entity.class, parser -> new BooleanParser(parser).parse())) + .put("Glowing", new Option(Entity.class, parser -> new BooleanParser(parser).parse())) + .put("TicksFrozen", new Option(Entity.class, parser -> new IntParser(parser).parse())) + .put("HasVisualFire", new Option(Entity.class, parser -> new BooleanParser(parser).parse())) + .put("Tags", new Option(Entity.class, parser -> new ListParser(parser, new StringParser(parser), NumberRange.IntRange.atMost(1024)).parse())) + .put("test", new Option(Entity.class, parser -> new ByteParser(parser).parse())) + + .build(); + + public static void suggestKeys(EntityParser parser, SuggestionsBuilder builder) { + String key = builder.getRemaining().toLowerCase(Locale.ROOT); + for (var option : OPTIONS.entrySet()) { + if (!option.getValue().shouldSuggest(parser) || !option.getKey().toLowerCase(Locale.ROOT).startsWith(key)) + continue; + builder.suggest(option.getKey() + ":"); + } + } + + public static boolean isPotentialKey(EntityParser parser, String key) { + boolean value = false; + for (var option : OPTIONS.entrySet()) { + if (!option.getValue().shouldSuggest(parser)) continue; + if (option.getKey().equals(key)) return false; + if (option.getValue().shouldSuggest(parser) + && option.getKey().toLowerCase(Locale.ROOT).startsWith(key.toLowerCase(Locale.ROOT))) { + value = true; + } + + } + return value; + } + + public static ValueHandler getValueHandler(EntityParser parser, String key, int cursor) { + Option option = OPTIONS.get(key); + if (option != null) { + if (option.shouldSuggest(parser)) { + return option.valueHandler; + } + } + parser.reader().setCursor(cursor); + // todo return nbt element handler by default + return null; + } + + public interface ValueHandler { + void handle(DefaultParser parser) throws CommandSyntaxException; + } + + static final class Option { + final Class requiresClass; + final ValueHandler valueHandler; + final Predicate predicate; + + Option(Class requiresClass, ValueHandler valueHandler) { + this(requiresClass, valueHandler, ignored -> true); + } + + Option(Class requiresClass, ValueHandler valueHandler, Predicate predicate) { + this.requiresClass = requiresClass; + this.valueHandler = valueHandler; + this.predicate = predicate; + } + + boolean shouldSuggest(EntityParser parser) { + if (!predicate.test(parser)) return false; + Class entityClass = parser.getEntityClass(); + while (!entityClass.equals(Object.class)) { + if (entityClass.equals(requiresClass)) { + return true; + } + entityClass = entityClass.getSuperclass(); + } + return false; + } + } +} diff --git a/src/client/java/works/nuty/calcite/parser/primitive/BooleanParser.java b/src/client/java/works/nuty/calcite/parser/primitive/BooleanParser.java new file mode 100644 index 0000000..f7bd7b2 --- /dev/null +++ b/src/client/java/works/nuty/calcite/parser/primitive/BooleanParser.java @@ -0,0 +1,37 @@ +package works.nuty.calcite.parser.primitive; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import works.nuty.calcite.parser.DefaultParser; + +import java.util.concurrent.CompletableFuture; + +public class BooleanParser extends DefaultParser { + private final DefaultParser parentParser; + + public BooleanParser(DefaultParser parentParser) { + super(parentParser.reader()); + this.parentParser = parentParser; + } + + private static CompletableFuture suggestBoolean(SuggestionsBuilder builder) { + builder.suggest("true"); + builder.suggest("false"); + return builder.buildFuture(); + } + + public void parse() throws CommandSyntaxException { + parentParser.suggest(BooleanParser::suggestBoolean); + final int start = parentParser.reader().getCursor(); + final String value = parentParser.reader().readString(); + if (value.isEmpty()) { + parentParser.reader().setCursor(start); + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerExpectedBool().createWithContext(parentParser.reader()); + } + if (!value.equals("true") && !value.equals("false")) { + parentParser.reader().setCursor(start); + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerExpectedBool().createWithContext(parentParser.reader()); + } + } +} diff --git a/src/client/java/works/nuty/calcite/parser/primitive/ByteParser.java b/src/client/java/works/nuty/calcite/parser/primitive/ByteParser.java new file mode 100644 index 0000000..5e2cdd2 --- /dev/null +++ b/src/client/java/works/nuty/calcite/parser/primitive/ByteParser.java @@ -0,0 +1,68 @@ +package works.nuty.calcite.parser.primitive; + +import com.mojang.brigadier.LiteralMessage; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import works.nuty.calcite.parser.DefaultParser; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.regex.Pattern; + +public class ByteParser extends DefaultParser { + private static final DynamicCommandExceptionType READER_INVALID_BYTE = new DynamicCommandExceptionType(value -> new LiteralMessage("Invalid byte '" + value + "'")); + private static final SimpleCommandExceptionType READER_EXPECTED_BYTE = new SimpleCommandExceptionType(new LiteralMessage("Expected byte")); + private static final Pattern BYTE_PATTERN = Pattern.compile("[-+]?(?:0|[1-9][0-9]*)b", Pattern.CASE_INSENSITIVE); + private final DefaultParser parentParser; + + public ByteParser(DefaultParser parentParser) { + super(parentParser.reader()); + this.parentParser = parentParser; + } + + private static Function> getByteSuggestionFunction(String value) { + return builder -> { + builder.suggest(value + "b"); + return builder.buildFuture(); + }; + } + + private static CompletableFuture suggestBoolean(SuggestionsBuilder builder) { + builder.suggest("true"); + builder.suggest("false"); + return builder.buildFuture(); + } + + private static String removeSuffix(String value) { + if (value.endsWith("b") || value.endsWith("B")) return value.substring(0, value.length() - 1); + return value; + } + + public void parse() throws CommandSyntaxException { + final int start = parentParser.reader().getCursor(); + final String value = parentParser.reader().readUnquotedString(); + parentParser.suggestNothing(); + if (value.isEmpty()) { + parentParser.reader().setCursor(start); + throw READER_EXPECTED_BYTE.createWithContext(parentParser.reader()); + } + try { + if (!value.startsWith("t") && !value.startsWith("f")) { + Byte.parseByte(removeSuffix(value)); + parentParser.suggest(getByteSuggestionFunction(value)); + } else { + parentParser.suggest(ByteParser::suggestBoolean); + } + } catch (NumberFormatException ignored) { + parentParser.reader().setCursor(start); + throw READER_INVALID_BYTE.createWithContext(parentParser.reader(), value); + } + if (!BYTE_PATTERN.matcher(value).matches() && !value.equals("true") && !value.equals("false")) { + parentParser.reader().setCursor(start); + throw READER_INVALID_BYTE.createWithContext(parentParser.reader(), value); + } + } +} diff --git a/src/client/java/works/nuty/calcite/parser/primitive/DoubleParser.java b/src/client/java/works/nuty/calcite/parser/primitive/DoubleParser.java new file mode 100644 index 0000000..460915c --- /dev/null +++ b/src/client/java/works/nuty/calcite/parser/primitive/DoubleParser.java @@ -0,0 +1,54 @@ +package works.nuty.calcite.parser.primitive; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import works.nuty.calcite.parser.DefaultParser; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.regex.Pattern; + +public class DoubleParser extends DefaultParser { + private static final Pattern DOUBLE_PATTERN_EXPLICIT = Pattern.compile("[-+]?(?:[0-9]+[.]?|[0-9]*[.][0-9]+)(?:e[-+]?[0-9]+)?d", Pattern.CASE_INSENSITIVE); + private static final Pattern DOUBLE_PATTERN_IMPLICIT = Pattern.compile("[-+]?(?:[0-9]+[.]|[0-9]*[.][0-9]+)(?:e[-+]?[0-9]+)?", Pattern.CASE_INSENSITIVE); + private final DefaultParser parentParser; + + public DoubleParser(DefaultParser parentParser) { + super(parentParser.reader()); + this.parentParser = parentParser; + } + + private static Function> getShortSuggestionFunction(String value) { + return builder -> { + builder.suggest(value + "d"); + return builder.buildFuture(); + }; + } + + private static String removeSuffix(String value) { + if (value.endsWith("d") || value.endsWith("D")) return value.substring(0, value.length() - 1); + return value; + } + + public void parse() throws CommandSyntaxException { + final int start = reader().getCursor(); + final String value = parentParser.reader().readUnquotedString(); + parentParser.suggestNothing(); + if (value.isEmpty()) { + parentParser.reader().setCursor(start); + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerExpectedFloat().createWithContext(parentParser.reader()); + } + try { + Float.parseFloat(removeSuffix(value)); + } catch (NumberFormatException ignored) { + parentParser.reader().setCursor(start); + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerInvalidFloat().createWithContext(parentParser.reader(), value); + } + parentParser.suggest(getShortSuggestionFunction(value)); + if (!DOUBLE_PATTERN_EXPLICIT.matcher(value).matches() && !DOUBLE_PATTERN_IMPLICIT.matcher(value).matches()) { + parentParser.reader().setCursor(start); + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerInvalidFloat().createWithContext(parentParser.reader(), value); + } + } +} diff --git a/src/client/java/works/nuty/calcite/parser/primitive/FloatParser.java b/src/client/java/works/nuty/calcite/parser/primitive/FloatParser.java new file mode 100644 index 0000000..7e565a7 --- /dev/null +++ b/src/client/java/works/nuty/calcite/parser/primitive/FloatParser.java @@ -0,0 +1,53 @@ +package works.nuty.calcite.parser.primitive; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import works.nuty.calcite.parser.DefaultParser; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.regex.Pattern; + +public class FloatParser extends DefaultParser { + private static final Pattern FLOAT_PATTERN = Pattern.compile("[-+]?(?:[0-9]+[.]?|[0-9]*[.][0-9]+)(?:e[-+]?[0-9]+)?f", Pattern.CASE_INSENSITIVE); + private final DefaultParser parentParser; + + public FloatParser(DefaultParser parentParser) { + super(parentParser.reader()); + this.parentParser = parentParser; + } + + private static Function> getShortSuggestionFunction(String value) { + return builder -> { + builder.suggest(value + "f"); + return builder.buildFuture(); + }; + } + + private static String removeSuffix(String value) { + if (value.endsWith("f") || value.endsWith("F")) return value.substring(0, value.length() - 1); + return value; + } + + public void parse() throws CommandSyntaxException { + final int start = parentParser.reader().getCursor(); + final String value = parentParser.reader().readUnquotedString(); + parentParser.suggestNothing(); + if (value.isEmpty()) { + parentParser.reader().setCursor(start); + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerExpectedFloat().createWithContext(parentParser.reader()); + } + try { + Float.parseFloat(removeSuffix(value)); + } catch (NumberFormatException ignored) { + parentParser.reader().setCursor(start); + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerInvalidFloat().createWithContext(parentParser.reader(), value); + } + parentParser.suggest(getShortSuggestionFunction(value)); + if (!FLOAT_PATTERN.matcher(value).matches()) { + parentParser.reader().setCursor(start); + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerInvalidFloat().createWithContext(parentParser.reader(), value); + } + } +} diff --git a/src/client/java/works/nuty/calcite/parser/primitive/IntParser.java b/src/client/java/works/nuty/calcite/parser/primitive/IntParser.java new file mode 100644 index 0000000..1644ecd --- /dev/null +++ b/src/client/java/works/nuty/calcite/parser/primitive/IntParser.java @@ -0,0 +1,30 @@ +package works.nuty.calcite.parser.primitive; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import works.nuty.calcite.parser.DefaultParser; + +public class IntParser extends DefaultParser { + private final DefaultParser parentParser; + + public IntParser(DefaultParser parentParser) { + super(parentParser.reader()); + this.parentParser = parentParser; + } + + + public void parse() throws CommandSyntaxException { + final int start = parentParser.reader().getCursor(); + final String value = parentParser.reader().readUnquotedString(); + parentParser.suggestNothing(); + if (value.isEmpty()) { + parentParser.reader().setCursor(start); + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerExpectedInt().createWithContext(parentParser.reader()); + } + try { + Integer.parseInt(value); + } catch (NumberFormatException ignored) { + parentParser.reader().setCursor(start); + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerInvalidInt().createWithContext(parentParser.reader(), value); + } + } +} diff --git a/src/client/java/works/nuty/calcite/parser/primitive/LongParser.java b/src/client/java/works/nuty/calcite/parser/primitive/LongParser.java new file mode 100644 index 0000000..53ffaba --- /dev/null +++ b/src/client/java/works/nuty/calcite/parser/primitive/LongParser.java @@ -0,0 +1,53 @@ +package works.nuty.calcite.parser.primitive; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import works.nuty.calcite.parser.DefaultParser; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.regex.Pattern; + +public class LongParser extends DefaultParser { + private static final Pattern LONG_PATTERN = Pattern.compile("[-+]?(?:0|[1-9][0-9]*)l", Pattern.CASE_INSENSITIVE); + private final DefaultParser parentParser; + + public LongParser(DefaultParser parentParser) { + super(parentParser.reader()); + this.parentParser = parentParser; + } + + private static Function> getLongSuggestionFunction(String value) { + return builder -> { + builder.suggest(value + "L"); + return builder.buildFuture(); + }; + } + + private static String removeSuffix(String value) { + if (value.endsWith("l") || value.endsWith("L")) return value.substring(0, value.length() - 1); + return value; + } + + public void parse() throws CommandSyntaxException { + final int start = parentParser.reader().getCursor(); + final String value = parentParser.reader().readUnquotedString(); + parentParser.suggestNothing(); + if (value.isEmpty()) { + parentParser.reader().setCursor(start); + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerExpectedLong().createWithContext(parentParser.reader()); + } + try { + Long.parseLong(removeSuffix(value)); + } catch (NumberFormatException ignored) { + parentParser.reader().setCursor(start); + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerInvalidLong().createWithContext(parentParser.reader(), value); + } + parentParser.suggest(getLongSuggestionFunction(value)); + if (!LONG_PATTERN.matcher(value).matches()) { + parentParser.reader().setCursor(start); + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerInvalidLong().createWithContext(parentParser.reader(), value); + } + } +} diff --git a/src/client/java/works/nuty/calcite/parser/primitive/ShortParser.java b/src/client/java/works/nuty/calcite/parser/primitive/ShortParser.java new file mode 100644 index 0000000..0e313be --- /dev/null +++ b/src/client/java/works/nuty/calcite/parser/primitive/ShortParser.java @@ -0,0 +1,58 @@ +package works.nuty.calcite.parser.primitive; + +import com.mojang.brigadier.LiteralMessage; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import works.nuty.calcite.parser.DefaultParser; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.regex.Pattern; + +public class ShortParser extends DefaultParser { + private static final DynamicCommandExceptionType READER_INVALID_SHORT = new DynamicCommandExceptionType(value -> new LiteralMessage("Invalid byte '" + value + "'")); + private static final SimpleCommandExceptionType READER_EXPECTED_SHORT = new SimpleCommandExceptionType(new LiteralMessage("Expected byte")); + private static final Pattern SHORT_PATTERN = Pattern.compile("[-+]?(?:0|[1-9][0-9]*)s", Pattern.CASE_INSENSITIVE); + private final DefaultParser parentParser; + + public ShortParser(DefaultParser parentParser) { + super(parentParser.reader()); + this.parentParser = parentParser; + } + + private static Function> getShortSuggestionFunction(String value) { + return builder -> { + builder.suggest(value + "s"); + return builder.buildFuture(); + }; + } + + private static String removeSuffix(String value) { + if (value.endsWith("s") || value.endsWith("S")) return value.substring(0, value.length() - 1); + return value; + } + + public void parse() throws CommandSyntaxException { + final int start = parentParser.reader().getCursor(); + final String value = parentParser.reader().readUnquotedString(); + parentParser.suggestNothing(); + if (value.isEmpty()) { + parentParser.reader().setCursor(start); + throw READER_EXPECTED_SHORT.createWithContext(parentParser.reader()); + } + try { + Short.parseShort(removeSuffix(value)); + } catch (NumberFormatException ignored) { + parentParser.reader().setCursor(start); + throw READER_INVALID_SHORT.createWithContext(parentParser.reader(), value); + } + parentParser.suggest(getShortSuggestionFunction(value)); + if (!SHORT_PATTERN.matcher(value).matches()) { + parentParser.reader().setCursor(start); + throw READER_INVALID_SHORT.createWithContext(parentParser.reader(), value); + } + } +} diff --git a/src/client/java/works/nuty/calcite/parser/primitive/StringParser.java b/src/client/java/works/nuty/calcite/parser/primitive/StringParser.java new file mode 100644 index 0000000..fae41b1 --- /dev/null +++ b/src/client/java/works/nuty/calcite/parser/primitive/StringParser.java @@ -0,0 +1,47 @@ +package works.nuty.calcite.parser.primitive; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import works.nuty.calcite.parser.DefaultParser; + +import java.util.concurrent.CompletableFuture; + +public class StringParser extends DefaultParser { + private final DefaultParser parentParser; + + public StringParser(DefaultParser parentParser) { + super(parentParser.reader()); + this.parentParser = parentParser; + } + + public static CompletableFuture suggestQuotes(SuggestionsBuilder builder) { + builder.suggest("\""); + builder.suggest("'"); + return builder.buildFuture(); + } + + public void parse() throws CommandSyntaxException { + parentParser.suggest(StringParser::suggestQuotes); + if (!parentParser.reader().canRead()) { + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerExpectedStartOfQuote().createWithContext(parentParser.reader()); + } + final char next = parentParser.reader().peek(); + if (StringReader.isQuotedStringStart(next)) { + parentParser.reader().skip(); + try { + parentParser.reader().readStringUntil(next); + } catch (CommandSyntaxException ignored) { + parentParser.suggest((builder -> { + builder.suggest(Character.toString(next)); + return builder.buildFuture(); + })); + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.readerExpectedEndOfQuote().createWithContext(parentParser.reader()); + } + } else { + parentParser.suggestNothing(); + parentParser.reader().readUnquotedString(); + } + } +} diff --git a/src/client/java/works/nuty/calcite/screen/CalciteCommandScreen.java b/src/client/java/works/nuty/calcite/screen/CalciteCommandScreen.java index e04c645..3693d45 100644 --- a/src/client/java/works/nuty/calcite/screen/CalciteCommandScreen.java +++ b/src/client/java/works/nuty/calcite/screen/CalciteCommandScreen.java @@ -1,5 +1,6 @@ package works.nuty.calcite.screen; +import com.mojang.brigadier.context.ParsedArgument; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.block.Block; @@ -11,25 +12,25 @@ import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.Element; import net.minecraft.client.gui.Selectable; -import net.minecraft.client.gui.screen.ChatInputSuggestor; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.widget.ButtonWidget; import net.minecraft.client.gui.widget.ClickableWidget; import net.minecraft.client.gui.widget.ElementListWidget; import net.minecraft.client.util.NarratorManager; +import net.minecraft.nbt.NbtCompound; import net.minecraft.network.packet.c2s.play.UpdateCommandBlockC2SPacket; import net.minecraft.screen.ScreenTexts; import net.minecraft.text.Text; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.Direction; +import net.minecraft.util.math.MathHelper; import net.minecraft.world.CommandBlockExecutor; import net.minecraft.world.World; import org.jetbrains.annotations.Nullable; import org.lwjgl.glfw.GLFW; -import works.nuty.calcite.mixin.client.ChatInputSuggestorFields; import works.nuty.calcite.mixin.client.ClientPlayNetworkHandlerFields; -import works.nuty.calcite.mixin.client.SuggestionWindowFields; import works.nuty.calcite.widget.AutoActivateButtonWidget; +import works.nuty.calcite.widget.CalciteInputSuggestor; import works.nuty.calcite.widget.CalciteTextFieldWidget; import works.nuty.calcite.widget.ModeButtonWidget; @@ -45,6 +46,7 @@ public class CalciteCommandScreen extends Screen { private ButtonWidget cancelButton; @Nullable private Runnable commandSuggestorRenderer; + private CommandListWidget oldCommandListWidget; public CalciteCommandScreen(CommandBlockBlockEntity initialBlockEntity) { super(NarratorManager.EMPTY); @@ -132,6 +134,8 @@ protected void init() { this.commandListWidget.positionedWidgets.get(blockEntity.getPos()).updateCommandBlock(); }); } + } else { + this.commandListWidget.apply(this.oldCommandListWidget); } this.doneButton = ButtonWidget.builder(ScreenTexts.DONE, button -> this.commitAndClose()) @@ -145,9 +149,14 @@ protected void init() { @Override public void resize(MinecraftClient client, int width, int height) { - CommandListWidget clw = this.commandListWidget; + this.oldCommandListWidget = this.commandListWidget; this.init(client, width, height); - this.commandListWidget.apply(clw); + } + + @Override + public void removed() { + super.removed(); + this.oldCommandListWidget = this.commandListWidget; } @Override @@ -352,7 +361,7 @@ public void scrollToFocused() { @Environment(EnvType.CLIENT) public class CommandWidget extends AbstractCommandWidget { - protected final ChatInputSuggestor commandSuggestor; + protected final CalciteInputSuggestor commandSuggestor; protected final List children; private final CommandBlockBlockEntity blockEntity; private final CalciteTextFieldWidget commandEdit; @@ -373,7 +382,7 @@ public CommandWidget(CommandBlockBlockEntity blockEntity) { this.commandEdit.setMaxLength(32500); this.commandEdit.setChangedListener(this::onCommandChanged); - this.commandSuggestor = new ChatInputSuggestor(CalciteCommandScreen.this.client, CalciteCommandScreen.this, this.commandEdit, CalciteCommandScreen.this.textRenderer, true, true, 0, 10, false, Integer.MIN_VALUE); + this.commandSuggestor = new CalciteInputSuggestor(CalciteCommandScreen.this.client, CalciteCommandScreen.this, this.commandEdit, CalciteCommandScreen.this.textRenderer, true, true, 0, 10, false, Integer.MIN_VALUE); this.commandSuggestor.setWindowActive(true); this.commandSuggestor.refresh(); @@ -484,6 +493,14 @@ public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmou @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { + ParsedArgument arg; + if ((arg = this.commandEdit.getArgumentAtMouse(MathHelper.floor(mouseX - 4), MathHelper.floor(mouseY))) != null) { + Object result = arg.getResult(); + if (result instanceof NbtCompound nbt) { + assert CalciteCommandScreen.this.client != null; + CalciteCommandScreen.this.client.setScreen(new CalciteNBTEditScreen(CalciteCommandScreen.this, nbt, arg.getRange(), this.commandEdit)); + } + } if (this.commandEdit.isFocused() && this.commandSuggestor.mouseClicked(mouseX, mouseY, button)) { return true; } @@ -509,14 +526,7 @@ public void render(DrawContext context, int index, int y, int x, int entryWidth, CalciteCommandScreen.this.commandSuggestorRenderer = () -> { context.getMatrices().push(); context.getMatrices().translate(3, 0, 1); - ChatInputSuggestor.SuggestionWindow window = ((ChatInputSuggestorFields) this.commandSuggestor).getWindow(); - if (window != null) { - ((SuggestionWindowFields) window).getArea().setY(calculateSuggestionY(y)); - } - if (!this.commandSuggestor.tryRenderWindow(context, mouseX, mouseY)) { - context.getMatrices().translate(-3, calculateMessageY(y), 0); - this.commandSuggestor.renderMessages(context); - } + this.commandSuggestor.render(context, y, mouseX, mouseY); context.getMatrices().pop(); }; } @@ -540,17 +550,5 @@ protected void syncSettingsToServer() { CommandBlockExecutor commandExecutor = blockEntity.getCommandExecutor(); CalciteCommandScreen.this.client.getNetworkHandler().sendPacket(new UpdateCommandBlockC2SPacket(BlockPos.ofFloored(commandExecutor.getPos()), this.commandEdit.getText(), this.mode, commandExecutor.isTrackingOutput(), this.conditional, this.autoActivate)); } - - private int calculateSuggestionY(int y) { - return height / 2 - 6 < y - ? y - 3 - Math.min(((SuggestionWindowFields) ((ChatInputSuggestorFields) this.commandSuggestor).getWindow()).getSuggestions().size(), ((ChatInputSuggestorFields) this.commandSuggestor).getMaxSuggestionSize()) * 12 - : (y + 24) - (this.commandEdit.drawsBackground() ? 1 : 0); - } - - private int calculateMessageY(int y) { - return (height / 2 - 6 < y - ? y - 3 - ((ChatInputSuggestorFields) this.commandSuggestor).getMessages().size() * 12 - : (y + 24) - (this.commandEdit.drawsBackground() ? 1 : 0)) - 72; - } } } \ No newline at end of file diff --git a/src/client/java/works/nuty/calcite/screen/CalciteNBTEditScreen.java b/src/client/java/works/nuty/calcite/screen/CalciteNBTEditScreen.java new file mode 100644 index 0000000..9a399bb --- /dev/null +++ b/src/client/java/works/nuty/calcite/screen/CalciteNBTEditScreen.java @@ -0,0 +1,47 @@ +package works.nuty.calcite.screen; + +import com.mojang.brigadier.context.StringRange; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.util.NarratorManager; +import net.minecraft.nbt.NbtCompound; +import works.nuty.calcite.VerticalNbtTextFormatter; +import works.nuty.calcite.widget.CalciteTextFieldWidget; + +public class CalciteNBTEditScreen extends Screen { + final private Screen parent; + final private NbtCompound nbt; + final private CalciteTextFieldWidget textField; + private StringRange range; + + protected CalciteNBTEditScreen(Screen parent, NbtCompound nbt, StringRange range, CalciteTextFieldWidget textField) { + super(NarratorManager.EMPTY); + this.parent = parent; + this.nbt = nbt; + this.range = range; + this.textField = textField; + } + + private void commitAndClose() { + String originalText = textField.getText(); + textField.setText(originalText.substring(0, range.getStart()) + nbt.toString() + originalText.substring(range.getEnd())); + this.close(); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + context.drawTooltip(this.textRenderer, new VerticalNbtTextFormatter(" ", 0).apply(this.nbt), 1, 10); + } + + @Override + public void close() { + assert this.client != null; + this.client.setScreenAndRender(this.parent); + } + + @Override + public boolean shouldCloseOnEsc() { + // todo set this to false; temporarily set to true for testing. + return true; + } +} diff --git a/src/client/java/works/nuty/calcite/suggestion/CalciteSuggestion.java b/src/client/java/works/nuty/calcite/suggestion/CalciteSuggestion.java new file mode 100644 index 0000000..7f95e96 --- /dev/null +++ b/src/client/java/works/nuty/calcite/suggestion/CalciteSuggestion.java @@ -0,0 +1,39 @@ +package works.nuty.calcite.suggestion; + +import com.mojang.brigadier.context.StringRange; +import com.mojang.brigadier.suggestion.Suggestion; + +public abstract class CalciteSuggestion extends Suggestion { + private StringRange range; + private String text; + + public CalciteSuggestion() { + super(StringRange.at(0), ""); + this.range = StringRange.at(0); + this.text = ""; + } + + public void setRange(StringRange range) { + this.range = range; + } + + public void setText(String text) { + this.text = text; + } + + @Override + public String apply(String input) { + if (range.getStart() == 0 && range.getEnd() == input.length()) { + return text; + } + final StringBuilder result = new StringBuilder(); + if (range.getStart() > 0) { + result.append(input, 0, range.getStart()); + } + result.append(text); + if (range.getEnd() < input.length()) { + result.append(input.substring(range.getEnd())); + } + return result.toString(); + } +} diff --git a/src/client/java/works/nuty/calcite/suggestion/CalciteSuggestionsBuilder.java b/src/client/java/works/nuty/calcite/suggestion/CalciteSuggestionsBuilder.java new file mode 100644 index 0000000..7139276 --- /dev/null +++ b/src/client/java/works/nuty/calcite/suggestion/CalciteSuggestionsBuilder.java @@ -0,0 +1,17 @@ +package works.nuty.calcite.suggestion; + +import com.mojang.brigadier.context.StringRange; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import works.nuty.calcite.mixin.client.SuggestionsBuilderFields; + +public class CalciteSuggestionsBuilder extends SuggestionsBuilder { + public CalciteSuggestionsBuilder(String input, String inputLowerCase, int start) { + super(input, inputLowerCase, start); + } + + public static void suggestCustom(SuggestionsBuilder target, CalciteSuggestion suggestion) { + suggestion.setRange(StringRange.between(target.getStart(), target.getInput().length())); + + ((SuggestionsBuilderFields) target).getResult().add(suggestion); + } +} diff --git a/src/client/java/works/nuty/calcite/suggestion/TypedSuggestionProvider.java b/src/client/java/works/nuty/calcite/suggestion/TypedSuggestionProvider.java new file mode 100644 index 0000000..3620a12 --- /dev/null +++ b/src/client/java/works/nuty/calcite/suggestion/TypedSuggestionProvider.java @@ -0,0 +1,17 @@ +package works.nuty.calcite.suggestion; + +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; + +import java.util.concurrent.CompletableFuture; + +public abstract class TypedSuggestionProvider implements SuggestionProvider { + abstract public String getType(); + + @Override + public CompletableFuture getSuggestions(CommandContext context, SuggestionsBuilder builder) { + return Suggestions.empty(); + } +} diff --git a/src/client/java/works/nuty/calcite/widget/CalciteInputSuggestor.java b/src/client/java/works/nuty/calcite/widget/CalciteInputSuggestor.java new file mode 100644 index 0000000..639516d --- /dev/null +++ b/src/client/java/works/nuty/calcite/widget/CalciteInputSuggestor.java @@ -0,0 +1,389 @@ +package works.nuty.calcite.widget; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.ParseResults; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.context.CommandContextBuilder; +import com.mojang.brigadier.context.ParsedArgument; +import com.mojang.brigadier.context.SuggestionContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestion; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.TextFieldWidget; +import net.minecraft.command.CommandSource; +import net.minecraft.server.command.CommandManager; +import net.minecraft.text.OrderedText; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.text.Texts; +import net.minecraft.util.Formatting; +import net.minecraft.util.math.MathHelper; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.glfw.GLFW; +import works.nuty.calcite.widget.suggestion.ListSuggestionWindow; +import works.nuty.calcite.widget.suggestion.SuggestionWindow; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +public class CalciteInputSuggestor { + private static final Pattern WHITESPACE_PATTERN = Pattern.compile("(\\s+)"); + private static final Style ERROR_STYLE; + private static final Style INFO_STYLE; + private static final List