Skip to content

Commit

Permalink
Added (team= ) modifier to entity filtering
Browse files Browse the repository at this point in the history
Matches entities based on vanilla scoreboard team internal name
or display name (case insensitive)

Also cleaned up EntityFilter.EntityMatcher class a bit
  • Loading branch information
desht committed Oct 8, 2023
1 parent 1f92924 commit 04a532b
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 49 deletions.
2 changes: 2 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Changes are in reverse chronological order; newest changes at the top.
* When suppressed, the Jet Boots display panel switches stats from green to red, and blinks them
* This will also happen if you try to fly more than 64 blocks above max build height (so Y > 384 for the overworld)
* PNC doesn't do this itself, but it adds a hook for other mods to use...
* Added new "team" modifier for entity filters: checks if the entity is on the given scoreboard team
* E.g. `@player(team=team1)` matches players on team "team1"

### Fixed
* When adding custom Heat Properties recipes, no longer require a mod matching the block's namespace to be loaded
Expand Down
87 changes: 39 additions & 48 deletions src/main/java/me/desht/pneumaticcraft/common/util/EntityFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import net.minecraft.world.item.DyeColor;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.scores.PlayerTeam;
import net.minecraftforge.common.IForgeShearable;
import net.minecraftforge.registries.ForgeRegistries;
import org.apache.commons.lang3.StringUtils;
Expand Down Expand Up @@ -224,9 +225,12 @@ private enum Modifier implements BiPredicate<Entity,String> {
ENTITY_TAG((str) -> true,
"any string tag (added to entities with the /tag command)",
Modifier::testEntityTag),
TYPE_TAG((str) -> true,
TYPE_TAG(ResourceLocation::isValidResourceLocation,
"any known entity type tag, e.g 'minecraft:skeletons'",
Modifier::testTypeTag);
Modifier::testTypeTag),
TEAM((str) -> true,
"any valid Minecraft team name",
Modifier::testTeamName);

private final Set<String> validationSet;
private final Predicate<String> validationPredicate;
Expand Down Expand Up @@ -280,6 +284,11 @@ private static boolean testTypeTag(Entity entity, String val) {
return entity.getType().is(key);
}

private static boolean testTeamName(Entity entity, String val) {
return entity.getTeam() instanceof PlayerTeam t
&& (t.getName().equalsIgnoreCase(val) || t.getDisplayName().getString().equalsIgnoreCase(val));
}

boolean isValid(String s) {
return validationPredicate == null ? validationSet.contains(s) : validationPredicate.test(s);
}
Expand Down Expand Up @@ -318,85 +327,67 @@ private static boolean isHeldItem(Entity entity, String name, boolean mainHand)
}

private static class EntityMatcher implements Predicate<Entity> {
private final Pattern regex;
private final Predicate<Entity> entityPredicate;
private final Predicate<Entity> matcher;
private final List<ModifierEntry> modifiers = new ArrayList<>();
private final boolean matchCustomName;

private EntityMatcher(String element) {
String[] splits = ELEMENT_SUBDIVIDER.split(element);
for (int i = 0; i < splits.length; i++) {
splits[i] = splits[i].trim();
}
List<String> splits = Arrays.stream(ELEMENT_SUBDIVIDER.split(element)).map(String::trim).toList();

if (splits[0].startsWith("@")) {
String arg0 = splits.get(0);
if (arg0.startsWith("@")) {
// match by entity predicate
String sub = splits[0].substring(1);
String sub = arg0.substring(1);
if (StringUtils.countMatches(element, "(") != StringUtils.countMatches(element, ")")) {
throw new IllegalArgumentException("Mismatched opening/closing braces");
}
entityPredicate = ENTITY_PREDICATES.get(sub);
Validate.isTrue(entityPredicate != null, "Unknown entity type specifier: @" + sub);
regex = null;
matchCustomName = false;
} else if (splits[0].length() > 2 && (splits[0].startsWith("\"") && splits[0].endsWith("\"") || splits[0].startsWith("'") && splits[0].endsWith("'"))) {
matcher = ENTITY_PREDICATES.get(sub);
Validate.isTrue(matcher != null, "Unknown entity type specifier: @" + sub);
} else if (arg0.length() > 2 && (arg0.startsWith("\"") && arg0.endsWith("\"") || arg0.startsWith("'") && arg0.endsWith("'"))) {
// match an entity with a custom name
entityPredicate = null;
regex = Pattern.compile(wildcardToRegex(splits[0].substring(1, splits[0].length() - 1)));
matchCustomName = true;
Pattern regex = Pattern.compile(wildcardToRegex(arg0.substring(1, arg0.length() - 1)));
matcher = e -> matchByName(e, regex);
} else {
// wildcard match on entity type name
entityPredicate = null;
regex = Pattern.compile(wildcardToRegex(splits[0]), Pattern.CASE_INSENSITIVE);
matchCustomName = false;
Pattern regex = Pattern.compile(wildcardToRegex(arg0), Pattern.CASE_INSENSITIVE);
matcher = e -> regex.matcher(PneumaticCraftUtils.getRegistryName(e).orElseThrow().getPath()).matches();
}

for (int i = 1; i < splits.length; i++) {
String[] parts = splits[i].split("=");
Validate.isTrue(parts.length == 2, "Invalid modifier syntax: " + splits[i]);
for (int i = 1; i < splits.size(); i++) {
String[] parts = splits.get(i).split("=");
Validate.isTrue(parts.length == 2, "Invalid modifier syntax: " + splits.get(i));
String key = parts[0], arg = parts[1];
boolean sense = true;
if (parts[0].endsWith("!")) {
parts[0] = parts[0].substring(0, parts[0].length() - 1);
if (key.endsWith("!")) {
key = key.substring(0, key.length() - 1);
sense = false;
}
Modifier modifier;
try {
modifier = Modifier.valueOf(parts[0].toUpperCase(Locale.ROOT));
Modifier modifier = Modifier.valueOf(key.toUpperCase(Locale.ROOT));
if (!modifier.isValid(arg)) {
throw new IllegalArgumentException(String.format("Invalid value '%s' for modifier '%s'. Valid values: %s",
arg, key, modifier.displayValidOptions()));
}
modifiers.add(new ModifierEntry(modifier, arg, sense));
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Unknown modifier: " + parts[0]);
throw new IllegalArgumentException("Unknown modifier: " + key);
}
if (!modifier.isValid(parts[1])) {
throw new IllegalArgumentException(String.format("Invalid value '%s' for modifier '%s'. Valid values: %s",
parts[1], parts[0], modifier.displayValidOptions()));
}
modifiers.add(new ModifierEntry(modifier, parts[1], sense));
}
}

@Override
public boolean test(Entity entity) {
boolean ok = false;
if (entityPredicate != null) {
ok = entityPredicate.test(entity);
} else if (regex != null) {
ok = matchCustomName ?
matchByName(entity, regex) :
regex.matcher(PneumaticCraftUtils.getRegistryName(entity).orElseThrow().getPath()).matches();
}
// modifiers test is a match-all (e.g. "sheep(sheared=false,color=black)" matches sheep which are unsheared AND black)
return ok && modifiers.stream().allMatch(modifierEntry -> modifierEntry.test(entity));
return matcher.test(entity) && modifiers.stream().allMatch(modifierEntry -> modifierEntry.test(entity));
}

private boolean matchByName(Entity entity, Pattern regex) {
private static boolean matchByName(Entity entity, Pattern regex) {
return entity instanceof Player player ?
player.getGameProfile().getName() != null && regex.matcher(player.getGameProfile().getName()).matches() :
entity.getCustomName() != null && regex.matcher(entity.getCustomName().getString()).matches();
}
}

private record ModifierEntry(Modifier modifier, String value,
boolean sense) implements Predicate<Entity> {

private record ModifierEntry(Modifier modifier, String value, boolean sense) implements Predicate<Entity> {
@Override
public boolean test(Entity e) {
return modifier.test(e, value) == sense;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
},
{
"type": "text",
"text": "All matches are case-insensitive, e.g. both $(#800)zombie/$ and $(#800)Zombie/$ will match zombies.$(p)You can specify a $(thing)sequence/$ of filters with the ';' (semicolon) separator - this is a $(italic)match any/$ function:$(li)$(#800)creeper;zombie/$ matches both Creepers $(italic)and/$ Zombies."
"text": "$(li)(#800)@player(team=team1)/$ matches players on scoreboard team \"team1\"$(p)All matches are case-insensitive, e.g. both $(#800)zombie/$ and $(#800)Zombie/$ will match zombies.$(p)You can specify a $(thing)sequence/$ of filters with the ';' (semicolon) separator - this is a $(italic)match any/$ function:$(li)$(#800)creeper;zombie/$ matches both Creepers $(italic)and/$ Zombies."
},
{
"type": "text",
Expand Down

0 comments on commit 04a532b

Please sign in to comment.