/* * This file is part of LanternServer, licensed under the MIT License (MIT). * * Copyright (c) LanternPowered <https://www.lanternpowered.org> * Copyright (c) SpongePowered <https://www.spongepowered.org> * Copyright (c) contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the Software), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.lanternpowered.server.text.selector; import static com.google.common.base.Preconditions.checkNotNull; import com.flowpowered.math.vector.Vector3d; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import org.spongepowered.api.Sponge; import org.spongepowered.api.command.CommandSource; import org.spongepowered.api.data.manipulator.mutable.DisplayNameData; import org.spongepowered.api.data.manipulator.mutable.entity.ExperienceHolderData; import org.spongepowered.api.data.manipulator.mutable.entity.GameModeData; import org.spongepowered.api.entity.Entity; import org.spongepowered.api.entity.EntityType; import org.spongepowered.api.entity.EntityTypes; import org.spongepowered.api.entity.living.player.Player; import org.spongepowered.api.entity.living.player.gamemode.GameMode; import org.spongepowered.api.entity.living.player.gamemode.GameModes; import org.spongepowered.api.scoreboard.Team; import org.spongepowered.api.scoreboard.TeamMember; import org.spongepowered.api.text.Text; import org.spongepowered.api.text.selector.Argument; import org.spongepowered.api.text.selector.Argument.Invertible; import org.spongepowered.api.text.selector.ArgumentHolder; import org.spongepowered.api.text.selector.ArgumentType; import org.spongepowered.api.text.selector.ArgumentTypes; import org.spongepowered.api.text.selector.Selector; import org.spongepowered.api.text.selector.SelectorType; import org.spongepowered.api.text.selector.SelectorTypes; import org.spongepowered.api.util.Functional; import org.spongepowered.api.world.Locatable; import org.spongepowered.api.world.Location; import org.spongepowered.api.world.World; import org.spongepowered.api.world.extent.Extent; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import javax.annotation.Nullable; /** * A resolver that acts like Vanilla Minecraft in many regards. */ public class SelectorResolver { private static final Function<CommandSource, String> GET_NAME = CommandSource::getName; private static final Vector3d ORIGIN = new Vector3d(0, 0, 0); private static final Set<ArgumentType<?>> LOCATION_BASED_ARGUMENTS; private static final Function<Number, Double> TO_DOUBLE = Number::doubleValue; private static final Collection<SelectorType> INFINITE_TYPES = ImmutableSet.of(SelectorTypes.ALL_ENTITIES, SelectorTypes.ALL_PLAYERS); static { ImmutableSet.Builder<ArgumentType<?>> builder = ImmutableSet.builder(); builder.addAll(ArgumentTypes.POSITION.getTypes()); builder.addAll(ArgumentTypes.DIMENSION.getTypes()); builder.addAll(ArgumentTypes.RADIUS.getTypes()); // Left commented because Vanilla doesn't include it (see field_179666_d) // builder.addAll(ArgumentTypes.ROTATION.getTypes()); LOCATION_BASED_ARGUMENTS = builder.build(); } private static Extent extentFromSource(CommandSource origin) { if (origin instanceof Locatable) { return ((Locatable) origin).getWorld(); } return null; } private static Vector3d positionFromSource(CommandSource origin) { if (origin instanceof Locatable) { return ((Locatable) origin).getLocation().getPosition(); } return null; } private static <I, R> Predicate<I> requireTypePredicate(Class<I> inputType, final Class<R> requiredType) { return requiredType::isInstance; } private static <E> Collection<E> asSet(Optional<E> opt) { if (opt.isPresent()) { return Collections.singleton(opt.get()); } return Collections.emptySet(); } private final Collection<Extent> extents; private final Vector3d position; private final Optional<CommandSource> original; private final Selector selector; private final Predicate<Entity> selectorFilter; private final boolean alwaysUsePosition; public SelectorResolver(Collection<? extends Extent> extents, Selector selector, boolean force) { this(extents, null, null, selector, force); } public SelectorResolver(Location<World> location, Selector selector, boolean force) { this(ImmutableSet.of(location.getExtent()), location.getPosition(), null, selector, force); } public SelectorResolver(CommandSource origin, Selector selector, boolean force) { this(asSet(Optional.ofNullable(extentFromSource(origin))), positionFromSource(origin), origin, selector, force); } private SelectorResolver(Collection<? extends Extent> extents, @Nullable Vector3d position, @Nullable CommandSource original, Selector selector, boolean force) { this.extents = ImmutableSet.copyOf(extents); this.position = position == null ? ORIGIN : position; this.original = Optional.ofNullable(original); this.selector = checkNotNull(selector); this.selectorFilter = makeFilter(); this.alwaysUsePosition = force; } private Predicate<Entity> makeFilter() { // for easier reading final Selector sel = this.selector; Vector3d position = getPositionOrDefault(this.position, ArgumentTypes.POSITION); List<Predicate<Entity>> filters = Lists.newArrayList(); addTypeFilters(filters); addDimensionFilters(position, filters); addRadiusFilters(position, filters); addLevelFilters(filters); addGamemodeFilters(filters); addNameFilters(filters); addRotationFilters(filters); addTeamFilters(filters); addScoreFilters(filters); SelectorType selectorType = sel.getType(); Optional<Argument.Invertible<EntityType>> type = sel.getArgument(ArgumentTypes.ENTITY_TYPE); // isn't an ALL_ENTITIES selector or it is a RANDOM selector for only players boolean isPlayerOnlySelector = selectorType == SelectorTypes.ALL_PLAYERS || selectorType == SelectorTypes.NEAREST_PLAYER || (selectorType == SelectorTypes.RANDOM && type.isPresent() && !type.get().isInverted() && type.get().getValue() != EntityTypes.PLAYER); if (isPlayerOnlySelector) { // insert at the start so it applies first filters.add(0, requireTypePredicate(Entity.class, Player.class)); } return Functional.predicateAnd(filters); } private void addDimensionFilters(final Vector3d position, List<Predicate<Entity>> filters) { Selector sel = this.selector; Vector3d boxDimensions = getPositionOrDefault(ORIGIN, ArgumentTypes.DIMENSION); Vector3d det2 = position.add(boxDimensions); final Vector3d boxMin = position.min(det2); final Vector3d boxMax = position.max(det2); if (sel.has(ArgumentTypes.DIMENSION.x())) { filters.add(input -> { Vector3d pos = input.getLocation().getPosition(); return pos.getX() >= boxMin.getX() && pos.getX() <= boxMax.getX(); }); } if (sel.has(ArgumentTypes.DIMENSION.y())) { filters.add(input -> { Vector3d pos = input.getLocation().getPosition(); return pos.getY() >= boxMin.getY() && pos.getY() <= boxMax.getY(); }); } if (sel.has(ArgumentTypes.DIMENSION.z())) { filters.add(input -> { Vector3d pos = input.getLocation().getPosition(); return pos.getZ() >= boxMin.getZ() && pos.getZ() <= boxMax.getZ(); }); } } private void addGamemodeFilters(List<Predicate<Entity>> filters) { Selector sel = this.selector; Optional<GameMode> gamemode = sel.get(ArgumentTypes.GAME_MODE); // If the game mode is NOT_SET, that means accept any if (gamemode.isPresent() && gamemode.get() != GameModes.NOT_SET) { final GameMode actualMode = gamemode.get(); filters.add(input -> { Optional<GameModeData> mode = input.get(GameModeData.class); return mode.isPresent() && mode.get() == actualMode; }); } } private void addLevelFilters(List<Predicate<Entity>> filters) { Selector sel = this.selector; Optional<Integer> levelMin = sel.get(ArgumentTypes.LEVEL.minimum()); Optional<Integer> levelMax = sel.get(ArgumentTypes.LEVEL.maximum()); if (levelMin.isPresent()) { final int actualMin = levelMin.get(); filters.add(input -> { Optional<ExperienceHolderData> xp = input.get(ExperienceHolderData.class); return xp.isPresent() && xp.get().level().get() >= actualMin; }); } if (levelMax.isPresent()) { final int actualMax = levelMax.get(); filters.add(input -> { Optional<ExperienceHolderData> xp = input.get(ExperienceHolderData.class); return xp.isPresent() && xp.get().level().get() <= actualMax; }); } } private void addNameFilters(List<Predicate<Entity>> filters) { Selector sel = this.selector; Optional<Argument.Invertible<String>> nameOpt = sel.getArgument(ArgumentTypes.NAME); if (nameOpt.isPresent()) { final String name = nameOpt.get().getValue(); final boolean inverted = nameOpt.get().isInverted(); filters.add(input -> { Optional<DisplayNameData> dispName = input.get(DisplayNameData.class); return inverted ^ (dispName.isPresent() && name.equals(dispName.get().displayName().get().toPlain())); }); } } private void addRadiusFilters(final Vector3d position, List<Predicate<Entity>> filters) { final Selector sel = this.selector; Optional<Integer> radiusMin = sel.get(ArgumentTypes.RADIUS.minimum()); Optional<Integer> radiusMax = sel.get(ArgumentTypes.RADIUS.maximum()); if (radiusMin.isPresent()) { int radMin = radiusMin.get(); int radMinSquared = radMin * radMin; filters.add(input -> input.getLocation().getPosition().distanceSquared(position) >= radMinSquared); } if (radiusMax.isPresent()) { int radMax = radiusMax.get(); int radMaxSquared = radMax * radMax; filters.add(input -> input.getLocation().getPosition().distanceSquared(position) <= radMaxSquared); } } private void addRotationFilters(List<Predicate<Entity>> filters) { Selector sel = this.selector; // If the Z's are uncommented, don't forget to implement them // Optional<Double> rotMinZ = sel.get(ArgumentTypes.ROTATION.minimum().z()); // Optional<Double> rotMaxZ = sel.get(ArgumentTypes.ROTATION.maximum().z()); Optional<Double> rotMinX = sel.get(ArgumentTypes.ROTATION.minimum().x()); if (rotMinX.isPresent()) { double rmx = rotMinX.get(); filters.add(input -> input.getRotation().getX() >= rmx); } Optional<Double> rotMinY = sel.get(ArgumentTypes.ROTATION.minimum().y()); if (rotMinY.isPresent()) { double rmy = rotMinY.get(); filters.add(input -> input.getRotation().getY() >= rmy); } Optional<Double> rotMaxX = sel.get(ArgumentTypes.ROTATION.maximum().x()); if (rotMaxX.isPresent()) { double rx = rotMaxX.get(); filters.add(input -> input.getRotation().getX() <= rx); } Optional<Double> rotMaxY = sel.get(ArgumentTypes.ROTATION.maximum().y()); if (rotMaxY.isPresent()) { double ry = rotMaxY.get(); filters.add(input -> input.getRotation().getY() <= ry); } } private void addScoreFilters(List<Predicate<Entity>> filters) { Selector sel = this.selector; sel.getArguments(); } private void addTeamFilters(List<Predicate<Entity>> filters) { Selector sel = this.selector; Optional<Invertible<String>> teamOpt = sel.getArgument(ArgumentTypes.TEAM); if (teamOpt.isPresent()) { Invertible<String> teamArg = teamOpt.get(); final boolean inverted = teamArg.isInverted(); final Collection<Team> teams = Sponge.getGame().getServer().getServerScoreboard().get().getTeams(); filters.add(new Predicate<Entity>() { @Override public boolean test(Entity input) { if (input instanceof TeamMember) { return inverted ^ collectMembers(teams).contains(((TeamMember) input).getTeamRepresentation()); } return false; } private Collection<Text> collectMembers(Collection<Team> teams) { ImmutableSet.Builder<Text> users = ImmutableSet.builder(); for (Team t : teams) { users.addAll(t.getMembers()); } return users.build(); } }); } } private void addTypeFilters(List<Predicate<Entity>> filters) { Selector sel = this.selector; Optional<Argument.Invertible<EntityType>> typeOpt = sel.getArgument(ArgumentTypes.ENTITY_TYPE); if (typeOpt.isPresent()) { Argument.Invertible<EntityType> typeArg = typeOpt.get(); boolean inverted = typeArg.isInverted(); EntityType type = typeArg.getValue(); filters.add(input -> inverted ^ input.getType() == type); } } private Vector3d getPositionOrDefault(Vector3d pos, ArgumentHolder.Vector3<?, ? extends Number> vecTypes) { Optional<Double> x = this.selector.get(vecTypes.x()).map(TO_DOUBLE); Optional<Double> y = this.selector.get(vecTypes.y()).map(TO_DOUBLE); Optional<Double> z = this.selector.get(vecTypes.z()).map(TO_DOUBLE); return new Vector3d(x.orElse(pos.getX()), y.orElse(pos.getY()), z.orElse(pos.getZ())); } public String getName() { return this.original.map(GET_NAME).orElse("SelectorResolver"); } public Set<Entity> resolve() { SelectorType selectorType = this.selector.getType(); int defaultCount = 1; if (INFINITE_TYPES.contains(selectorType)) { defaultCount = 0; } int maxToSelect = this.selector.get(ArgumentTypes.COUNT).orElse(defaultCount); Set<? extends Extent> extents = getExtentSet(); int count = 0; ImmutableSet.Builder<Entity> entities = ImmutableSet.builder(); for (Extent extent : extents) { Collection<Entity> allEntities = extent.getEntities(); if (selectorType == SelectorTypes.RANDOM) { List<Entity> entityList = new ArrayList<Entity>(allEntities); Collections.shuffle(entityList); allEntities = entityList; } for (Entity e : allEntities) { if (!this.selectorFilter.test(e)) { continue; } entities.add(e); count++; if (maxToSelect != 0 && count > maxToSelect) { break; } } } return entities.build(); } private Set<? extends Extent> getExtentSet() { if (!this.alwaysUsePosition && Collections.disjoint(getArgumentTypes(this.selector.getArguments()), LOCATION_BASED_ARGUMENTS)) { return ImmutableSet.copyOf(Sponge.getServer().getWorlds()); } return ImmutableSet.copyOf(this.extents); } private Collection<ArgumentType<?>> getArgumentTypes(Collection<Argument<?>> arguments) { return arguments.stream().map(Argument::getType).collect(Collectors.toSet()); } }