diff --git a/fabric-command-api-v2/src/main/java/net/fabricmc/fabric/api/command/v2/EntitySelectorOptionRegistry.java b/fabric-command-api-v2/src/main/java/net/fabricmc/fabric/api/command/v2/EntitySelectorOptionRegistry.java new file mode 100644 index 000000000..01caa7e47 --- /dev/null +++ b/fabric-command-api-v2/src/main/java/net/fabricmc/fabric/api/command/v2/EntitySelectorOptionRegistry.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.api.command.v2; + +import java.util.function.Predicate; + +import net.minecraft.command.EntitySelectorOptions; +import net.minecraft.command.EntitySelectorReader; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import net.fabricmc.fabric.mixin.command.EntitySelectorOptionsAccessor; + +/** + * Contains a function to register an entity selector option. + */ +public final class EntitySelectorOptionRegistry { + private EntitySelectorOptionRegistry() { + } + + /** + * Registers an entity selector option. The added option is available under the underscore + * separated ID. + * + * <p>Here's an example of a custom entity selector option. The option is registered under + * {@code example_min_health} and can be used like {@code @e[example_min_health=5]}. + * <pre>{@code + * EntitySelectorOptionRegistry.register( + * new Identifier("example", "min_health"), + * Text.literal("Minimum entity health"), + * (reader) -> { + * final float minHealth = reader.getReader().readFloat(); + * + * if (minHealth > 0) { + * reader.setPredicate((entity) -> entity instanceof LivingEntity livingEntity && livingEntity.getHealth() >= minHealth); + * } + * }, + * (reader) -> true + * ); + * }</pre> + * + * <p>By default, a selector option can be used multiple times. To make a non-repeatable + * option, either use {@link FabricEntitySelectorReader} to flag the existence of an option + * and check it inside {@code canUse}, or use {@link #registerNonRepeatable} instead of this + * method. + * + * @param id the ID of the option + * @param description the description of the option + * @param handler the handler for the entity option that reads and sets the predicate + * @param canUse the predicate that checks whether the option is syntactically valid + */ + public static void register(Identifier id, Text description, EntitySelectorOptions.SelectorHandler handler, Predicate<EntitySelectorReader> canUse) { + EntitySelectorOptionsAccessor.callPutOption(id.toUnderscoreSeparatedString(), handler, canUse, description); + } + + /** + * Registers an entity selector option. The added option is available under the underscore + * separated ID. The added option cannot be used multiple times within a single selector. + * + * @param id the ID of the option + * @param description the description of the option + * @param handler the handler for the entity option that reads and sets the predicate + */ + public static void registerNonRepeatable(Identifier id, Text description, EntitySelectorOptions.SelectorHandler handler) { + register(id, description, (reader) -> { + handler.handle(reader); + reader.setCustomFlag(id, true); + }, (reader) -> !reader.getCustomFlag(id)); // has a flag = used before + } +} diff --git a/fabric-command-api-v2/src/main/java/net/fabricmc/fabric/api/command/v2/FabricEntitySelectorReader.java b/fabric-command-api-v2/src/main/java/net/fabricmc/fabric/api/command/v2/FabricEntitySelectorReader.java new file mode 100644 index 000000000..1462c1a47 --- /dev/null +++ b/fabric-command-api-v2/src/main/java/net/fabricmc/fabric/api/command/v2/FabricEntitySelectorReader.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.api.command.v2; + +import net.minecraft.util.Identifier; + +/** + * Fabric extension to {@link net.minecraft.command.EntitySelectorReader}, implemented + * using interface injection. This allows custom entity selectors to + * set a custom flag to a reader. This can be used to implement mutually-exclusive + * or non-repeatable entity selector option. + */ +public interface FabricEntitySelectorReader { + /** + * Sets a flag. + * @param key the key of the flag + * @param value the value of the flag + */ + default void setCustomFlag(Identifier key, boolean value) { + throw new UnsupportedOperationException("Implemented via mixin"); + } + + /** + * Gets the value of the flag. + * @param key the key of the flag + * @return the value, or {@code false} if the flag is not set + */ + default boolean getCustomFlag(Identifier key) { + throw new UnsupportedOperationException("Implemented via mixin"); + } +} diff --git a/fabric-command-api-v2/src/main/java/net/fabricmc/fabric/mixin/command/EntitySelectorOptionsAccessor.java b/fabric-command-api-v2/src/main/java/net/fabricmc/fabric/mixin/command/EntitySelectorOptionsAccessor.java new file mode 100644 index 000000000..ccaee74a2 --- /dev/null +++ b/fabric-command-api-v2/src/main/java/net/fabricmc/fabric/mixin/command/EntitySelectorOptionsAccessor.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.mixin.command; + +import java.util.function.Predicate; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +import net.minecraft.command.EntitySelectorOptions; +import net.minecraft.command.EntitySelectorReader; +import net.minecraft.text.Text; + +@Mixin(EntitySelectorOptions.class) +public interface EntitySelectorOptionsAccessor { + @Invoker + static void callPutOption(String id, EntitySelectorOptions.SelectorHandler handler, Predicate<EntitySelectorReader> condition, Text description) { + } +} diff --git a/fabric-command-api-v2/src/main/java/net/fabricmc/fabric/mixin/command/EntitySelectorReaderMixin.java b/fabric-command-api-v2/src/main/java/net/fabricmc/fabric/mixin/command/EntitySelectorReaderMixin.java new file mode 100644 index 000000000..3cb8f8ed9 --- /dev/null +++ b/fabric-command-api-v2/src/main/java/net/fabricmc/fabric/mixin/command/EntitySelectorReaderMixin.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.mixin.command; + +import java.util.HashSet; +import java.util.Set; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +import net.minecraft.command.EntitySelectorReader; +import net.minecraft.util.Identifier; + +import net.fabricmc.fabric.api.command.v2.FabricEntitySelectorReader; + +@Mixin(EntitySelectorReader.class) +public class EntitySelectorReaderMixin implements FabricEntitySelectorReader { + @Unique + private final Set<Identifier> flags = new HashSet<>(); + + @Override + public void setCustomFlag(Identifier key, boolean value) { + if (value) { + this.flags.add(key); + } else { + this.flags.remove(key); + } + } + + @Override + public boolean getCustomFlag(Identifier key) { + return this.flags.contains(key); + } +} diff --git a/fabric-command-api-v2/src/main/resources/fabric-command-api-v2.mixins.json b/fabric-command-api-v2/src/main/resources/fabric-command-api-v2.mixins.json index f011f37c1..0117a3d14 100644 --- a/fabric-command-api-v2/src/main/resources/fabric-command-api-v2.mixins.json +++ b/fabric-command-api-v2/src/main/resources/fabric-command-api-v2.mixins.json @@ -5,6 +5,8 @@ "mixins": [ "ArgumentTypesAccessor", "CommandManagerMixin", + "EntitySelectorOptionsAccessor", + "EntitySelectorReaderMixin", "HelpCommandAccessor" ], "injectors": { diff --git a/fabric-command-api-v2/src/main/resources/fabric.mod.json b/fabric-command-api-v2/src/main/resources/fabric.mod.json index 8c2eaac2a..fdda5fe2a 100644 --- a/fabric-command-api-v2/src/main/resources/fabric.mod.json +++ b/fabric-command-api-v2/src/main/resources/fabric.mod.json @@ -30,6 +30,9 @@ } ], "custom": { - "fabric-api:module-lifecycle": "stable" + "fabric-api:module-lifecycle": "stable", + "loom:injected_interfaces": { + "net/minecraft/class_2303": ["net/fabricmc/fabric/api/command/v2/FabricEntitySelectorReader"] + } } } diff --git a/fabric-command-api-v2/src/testmod/java/net/fabricmc/fabric/test/command/CommandTest.java b/fabric-command-api-v2/src/testmod/java/net/fabricmc/fabric/test/command/CommandTest.java index ced6318bc..748ad298c 100644 --- a/fabric-command-api-v2/src/testmod/java/net/fabricmc/fabric/test/command/CommandTest.java +++ b/fabric-command-api-v2/src/testmod/java/net/fabricmc/fabric/test/command/CommandTest.java @@ -26,15 +26,19 @@ import com.mojang.brigadier.tree.RootCommandNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import net.minecraft.entity.LivingEntity; import net.minecraft.server.command.ServerCommandSource; import net.minecraft.text.Text; +import net.minecraft.util.Identifier; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.command.v2.EntitySelectorOptionRegistry; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; public final class CommandTest implements ModInitializer { private static final Logger LOGGER = LoggerFactory.getLogger(CommandTest.class); + static final Identifier SELECTOR_ID = new Identifier("fabric-command-api-v2-testmod", "min_health"); private static final SimpleCommandExceptionType WRONG_SIDE_SHOULD_BE_INTEGRATED = new SimpleCommandExceptionType(Text.literal("This command was registered incorrectly. Should only be present on an integrated server but was ran on a dedicated server!")); private static final SimpleCommandExceptionType WRONG_SIDE_SHOULD_BE_DEDICATED = new SimpleCommandExceptionType(Text.literal("This command was registered incorrectly. Should only be present on an dedicated server but was ran on an integrated server!")); @@ -50,7 +54,7 @@ public final class CommandTest implements ModInitializer { } if (environment.integrated) { - // The command here should only be present on a integrated server + // The command here should only be present on an integrated server dispatcher.register(literal("fabric_integrated_test_command").executes(this::executeIntegratedCommand)); } }); @@ -95,6 +99,18 @@ public final class CommandTest implements ModInitializer { // Success! CommandTest.LOGGER.info("The command tests have passed! Please make sure you execute the three commands for extra safety."); }); + + EntitySelectorOptionRegistry.registerNonRepeatable( + SELECTOR_ID, + Text.literal("Minimum entity health"), + (reader) -> { + final float minHealth = reader.getReader().readFloat(); + + if (minHealth > 0) { + reader.setPredicate((entity) -> entity instanceof LivingEntity livingEntity && livingEntity.getHealth() >= minHealth); + } + } + ); } private int executeCommonCommand(CommandContext<ServerCommandSource> context) { diff --git a/fabric-command-api-v2/src/testmod/java/net/fabricmc/fabric/test/command/EntitySelectorGameTest.java b/fabric-command-api-v2/src/testmod/java/net/fabricmc/fabric/test/command/EntitySelectorGameTest.java new file mode 100644 index 000000000..a0bcbdf20 --- /dev/null +++ b/fabric-command-api-v2/src/testmod/java/net/fabricmc/fabric/test/command/EntitySelectorGameTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.test.command; + +import java.util.Locale; + +import net.minecraft.entity.EntityType; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.server.MinecraftServer; +import net.minecraft.test.GameTest; +import net.minecraft.test.TestContext; +import net.minecraft.util.math.BlockPos; + +import net.fabricmc.fabric.api.gametest.v1.FabricGameTest; + +public class EntitySelectorGameTest { + private void spawn(TestContext context, float health) { + MobEntity entity = context.spawnMob(EntityType.CREEPER, BlockPos.ORIGIN); + entity.setAiDisabled(true); + entity.setHealth(health); + } + + @GameTest(templateName = FabricGameTest.EMPTY_STRUCTURE) + public void testEntitySelector(TestContext context) { + BlockPos absolute = context.getAbsolutePos(BlockPos.ORIGIN); + + spawn(context, 1.0f); + spawn(context, 5.0f); + spawn(context, 10.0f); + + String command = String.format( + Locale.ROOT, + "/kill @e[x=%d, y=%d, z=%d, distance=..2, %s=5.0]", + absolute.getX(), + absolute.getY(), + absolute.getZ(), + CommandTest.SELECTOR_ID.toUnderscoreSeparatedString() + ); + + context.expectEntitiesAround(EntityType.CREEPER, BlockPos.ORIGIN, 3, 2.0); + MinecraftServer server = context.getWorld().getServer(); + int result = server.getCommandManager().executeWithPrefix(server.getCommandSource(), command); + context.assertTrue(result == 2, "Expected 2 entities killed, got " + result); + context.expectEntitiesAround(EntityType.CREEPER, BlockPos.ORIGIN, 1, 2.0); + context.complete(); + } +} diff --git a/fabric-command-api-v2/src/testmod/resources/fabric.mod.json b/fabric-command-api-v2/src/testmod/resources/fabric.mod.json index 1acfc5897..3c9bfda09 100644 --- a/fabric-command-api-v2/src/testmod/resources/fabric.mod.json +++ b/fabric-command-api-v2/src/testmod/resources/fabric.mod.json @@ -15,6 +15,9 @@ ], "client": [ "net.fabricmc.fabric.test.command.client.ClientCommandTest" + ], + "fabric-gametest": [ + "net.fabricmc.fabric.test.command.EntitySelectorGameTest" ] } }