Entity Selector Option API ()

* Entity Selector Option API

* Fix compile error

* Add default impl

* Apply suggestions from code review

Co-authored-by: Technici4n <13494793+Technici4n@users.noreply.github.com>

Co-authored-by: modmuss50 <modmuss50@gmail.com>
Co-authored-by: Technici4n <13494793+Technici4n@users.noreply.github.com>
This commit is contained in:
apple502j 2023-01-05 21:49:30 +09:00 committed by GitHub
parent 5176f73dbb
commit 3fc4752e4f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 297 additions and 2 deletions

View file

@ -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
}
}

View file

@ -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");
}
}

View file

@ -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) {
}
}

View file

@ -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);
}
}

View file

@ -5,6 +5,8 @@
"mixins": [
"ArgumentTypesAccessor",
"CommandManagerMixin",
"EntitySelectorOptionsAccessor",
"EntitySelectorReaderMixin",
"HelpCommandAccessor"
],
"injectors": {

View file

@ -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"]
}
}
}

View file

@ -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) {

View file

@ -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();
}
}

View file

@ -15,6 +15,9 @@
],
"client": [
"net.fabricmc.fabric.test.command.client.ClientCommandTest"
],
"fabric-gametest": [
"net.fabricmc.fabric.test.command.EntitySelectorGameTest"
]
}
}