package tc.oc.pgm.filters.parser; import java.time.Duration; import java.util.Optional; import javax.inject.Inject; import javax.inject.Provider; import com.google.common.collect.Range; import org.bukkit.PoseFlag; import org.bukkit.attribute.Attribute; import org.bukkit.entity.EntityType; import org.bukkit.entity.LivingEntity; import org.bukkit.event.entity.CreatureSpawnEvent; import org.bukkit.inventory.ImItemStack; import org.jdom2.Element; import tc.oc.api.docs.SemanticVersion; import tc.oc.commons.bukkit.localization.MessageTemplate; import tc.oc.commons.core.formatting.StringUtils; import tc.oc.commons.core.util.Comparables; import tc.oc.pgm.classes.ClassModule; import tc.oc.pgm.classes.PlayerClass; import tc.oc.pgm.features.FeatureDefinitionContext; import tc.oc.pgm.features.FeatureDefinitionParser; import tc.oc.pgm.features.FeatureParser; import tc.oc.pgm.features.MagicMethodFeatureParser; import tc.oc.pgm.filters.Filter; import tc.oc.pgm.filters.matcher.CauseFilter; import tc.oc.pgm.filters.matcher.StaticFilter; import tc.oc.pgm.filters.matcher.block.MaterialFilter; import tc.oc.pgm.filters.matcher.block.StructuralLoadFilter; import tc.oc.pgm.filters.matcher.block.VoidFilter; import tc.oc.pgm.filters.matcher.damage.AttackerFilter; import tc.oc.pgm.filters.matcher.damage.DamagerFilter; import tc.oc.pgm.filters.matcher.damage.RelationFilter; import tc.oc.pgm.filters.matcher.damage.VictimFilter; import tc.oc.pgm.filters.matcher.entity.EntityTypeFilter; import tc.oc.pgm.filters.matcher.entity.SpawnReasonFilter; import tc.oc.pgm.filters.matcher.match.FlagStateFilter; import tc.oc.pgm.filters.matcher.match.LegacyRandomFilter; import tc.oc.pgm.filters.matcher.match.MatchMutationFilter; import tc.oc.pgm.filters.matcher.match.MatchStateFilter; import tc.oc.pgm.filters.matcher.match.MonostableFilter; import tc.oc.pgm.filters.matcher.match.PlayerCountFilter; import tc.oc.pgm.filters.matcher.match.RandomFilter; import tc.oc.pgm.filters.matcher.party.CompetitorFilter; import tc.oc.pgm.filters.matcher.party.GoalFilter; import tc.oc.pgm.filters.matcher.party.RankFilter; import tc.oc.pgm.filters.matcher.party.ScoreFilter; import tc.oc.pgm.filters.matcher.party.TeamFilter; import tc.oc.pgm.filters.matcher.player.AttributeFilter; import tc.oc.pgm.filters.matcher.player.CanFlyFilter; import tc.oc.pgm.filters.matcher.player.CarryingFlagFilter; import tc.oc.pgm.filters.matcher.player.CarryingItemFilter; import tc.oc.pgm.filters.matcher.player.HoldingItemFilter; import tc.oc.pgm.filters.matcher.player.KillStreakFilter; import tc.oc.pgm.filters.matcher.player.ParticipatingFilter; import tc.oc.pgm.filters.matcher.player.PlayerClassFilter; import tc.oc.pgm.filters.matcher.player.PoseFilter; import tc.oc.pgm.filters.matcher.player.WearingItemFilter; import tc.oc.pgm.filters.operator.AllFilter; import tc.oc.pgm.filters.operator.AnyFilter; import tc.oc.pgm.filters.operator.FallthroughFilter; import tc.oc.pgm.filters.operator.InverseFilter; import tc.oc.pgm.filters.operator.OneFilter; import tc.oc.pgm.filters.operator.SameTeamFilter; import tc.oc.pgm.filters.operator.TeamFilterAdapter; import tc.oc.pgm.flag.FlagDefinition; import tc.oc.pgm.flag.Post; import tc.oc.pgm.flag.state.Captured; import tc.oc.pgm.flag.state.Carried; import tc.oc.pgm.flag.state.Dropped; import tc.oc.pgm.flag.state.Returned; import tc.oc.pgm.flag.state.State; import tc.oc.pgm.goals.GoalDefinition; import tc.oc.pgm.itemmeta.ItemModifier; import tc.oc.pgm.kits.ItemParser; import tc.oc.pgm.map.MapProto; import tc.oc.pgm.map.ProtoVersions; import tc.oc.pgm.match.MatchState; import tc.oc.pgm.match.PlayerRelation; import tc.oc.pgm.mutation.Mutation; import tc.oc.pgm.teams.TeamFactory; import tc.oc.pgm.utils.MethodParser; import tc.oc.pgm.utils.XMLUtils; import tc.oc.pgm.xml.InvalidXMLException; import tc.oc.pgm.xml.Node; import tc.oc.pgm.xml.parser.Parser; import tc.oc.pgm.xml.property.MessageTemplateProperty; import tc.oc.pgm.xml.property.PropertyBuilderFactory; import static tc.oc.commons.core.exception.LambdaExceptionUtils.rethrowFunction; public class FilterDefinitionParser extends MagicMethodFeatureParser<Filter> implements FeatureDefinitionParser<Filter> { @Inject protected @MapProto SemanticVersion proto; @Inject protected FeatureDefinitionContext features; @Inject protected ItemParser itemParser; @Inject protected Parser<Attribute> attributeParser; @Inject protected PropertyBuilderFactory<MessageTemplate, MessageTemplateProperty> messageTemplates; @Inject protected CauseFilter.Factory causeFilters; @Inject protected FilterParser filterParser; @Inject protected FeatureParser<TeamFactory> teamParser; @Inject protected Provider<ClassModule> classModule; @Inject protected ItemModifier itemModifier; @MethodParser("allow") public Filter parseAllow(Element el) throws InvalidXMLException { return new FallthroughFilter(Filter.QueryResponse.ALLOW, filterParser.parseChild(el)); } @MethodParser("deny") public Filter parseDeny(Element el) throws InvalidXMLException { return new FallthroughFilter(Filter.QueryResponse.DENY, filterParser.parseChild(el)); } @MethodParser("always") public Filter parseAlways(Element el) { return new StaticFilter(Filter.QueryResponse.ALLOW); } @MethodParser("never") public Filter parseNever(Element el) { return new StaticFilter(Filter.QueryResponse.DENY); } @MethodParser("any") public Filter parseAny(Element el) throws InvalidXMLException { return new AnyFilter(filterParser.parseChildList(el)); } @MethodParser("all") public Filter parseAll(Element el) throws InvalidXMLException { return new AllFilter(filterParser.parseChildList(el)); } @MethodParser("one") public Filter parseOne(Element el) throws InvalidXMLException { return new OneFilter(filterParser.parseChildList(el)); } @MethodParser("not") public Filter parseNot(Element el) throws InvalidXMLException { return new InverseFilter(filterParser.parseChild(el)); } @MethodParser("team") public TeamFilter parseTeam(Element el) throws InvalidXMLException { return new TeamFilter(teamParser.parseReference(Node.of(el))); } @MethodParser("same-team") public SameTeamFilter parseSameTeam(Element el) throws InvalidXMLException { return new SameTeamFilter(filterParser.parseChild(el)); } @MethodParser("attacker") public AttackerFilter parseAttacker(Element el) throws InvalidXMLException { return new AttackerFilter(filterParser.parseChild(el)); } @MethodParser("victim") public VictimFilter parseVictim(Element el) throws InvalidXMLException { return new VictimFilter(filterParser.parseChild(el)); } @MethodParser public Filter damager(Element el) throws InvalidXMLException { return new DamagerFilter(filterParser.parseChild(el)); } @MethodParser("class") public PlayerClassFilter parseClass(Element el) throws InvalidXMLException { final PlayerClass playerClass = StringUtils.bestFuzzyMatch(el.getTextNormalize(), classModule.get().getPlayerClasses(), 0.9); if (playerClass == null) { throw new InvalidXMLException("Could not find player-class: " + el.getTextNormalize(), el); } return new PlayerClassFilter(playerClass); } @MethodParser("material") public MaterialFilter parseMaterial(Element el) throws InvalidXMLException { return new MaterialFilter(XMLUtils.parseMaterialPattern(el)); } @MethodParser("void") public VoidFilter parseVoid(Element el) throws InvalidXMLException { return new VoidFilter(); } @MethodParser("entity") public EntityTypeFilter parseEntity(Element el) throws InvalidXMLException { return new EntityTypeFilter(XMLUtils.parseEnum(el, EntityType.class, "entity type")); } @MethodParser("mob") public EntityTypeFilter parseMob(Element el) throws InvalidXMLException { EntityTypeFilter matcher = this.parseEntity(el); if(!LivingEntity.class.isAssignableFrom(matcher.getEntityType())) { throw new InvalidXMLException("Unknown mob type: " + el.getTextNormalize(), el); } return matcher; } @MethodParser("spawn") public SpawnReasonFilter parseSpawnReason(Element el) throws InvalidXMLException { return new SpawnReasonFilter(XMLUtils.parseEnum(new Node(el), CreatureSpawnEvent.SpawnReason.class, "spawn reason")); } @MethodParser("kill-streak") public KillStreakFilter parseKillStreak(Element el) throws InvalidXMLException { boolean repeat = XMLUtils.parseBoolean(el.getAttribute("repeat"), false); boolean persistent = XMLUtils.parseBoolean(el.getAttribute("persistent"), false); Integer count = XMLUtils.parseNumber(el.getAttribute("count"), Integer.class, (Integer) null); Integer min = XMLUtils.parseNumber(el.getAttribute("min"), Integer.class, (Integer) null); Integer max = XMLUtils.parseNumber(el.getAttribute("max"), Integer.class, (Integer) null); Range<Integer> range; if(count != null) { range = Range.singleton(count); } else if(min == null) { if(max == null) { throw new InvalidXMLException("kill-streak filter must have a count, min, or max", el); } else { range = Range.atMost(max); } } else { if(max == null) { range = Range.atLeast(min); } else { range = Range.closed(min, max); } } if(repeat && !range.hasUpperBound()) { throw new InvalidXMLException("repeating kill-streak filter must have a count or max", el); } return new KillStreakFilter(range, repeat, persistent); } @MethodParser("random") public Filter parseRandom(Element el) throws InvalidXMLException { Node node = new Node(el); Range<Double> chance; try { chance = Range.closedOpen(0d, XMLUtils.parseNumber(node, Double.class)); } catch(InvalidXMLException e) { chance = XMLUtils.parseNumericRange(node, Double.class); } Range<Double> valid = Range.closed(0d, 1d); if (valid.encloses(chance)) { return proto.isNoOlderThan(ProtoVersions.EVENT_QUERIES) ? new RandomFilter(chance) : new LegacyRandomFilter(chance); } else { double lower = chance.hasLowerBound() ? chance.lowerEndpoint() : Double.NEGATIVE_INFINITY; double upper = chance.hasUpperBound() ? chance.upperEndpoint() : Double.POSITIVE_INFINITY; double invalid; if(!valid.contains(lower)) { invalid = lower; } else { invalid = upper; } throw new InvalidXMLException("chance value (" + invalid + ") is not between 0 and 1", el); } } @MethodParser("grounded") public Filter parseGrounded(Element el) throws InvalidXMLException { return new PoseFilter(PoseFlag.GROUNDED); } @MethodParser({"crouching", "sneaking"}) public Filter parseCrouching(Element el) throws InvalidXMLException { return new PoseFilter(PoseFlag.SNEAKING); } @MethodParser("walking") public Filter parseWalking(Element el) throws InvalidXMLException { return new InverseFilter(new AnyFilter(new PoseFilter(PoseFlag.SNEAKING), new PoseFilter(PoseFlag.SPRINTING))); } @MethodParser("sprinting") public Filter parseSprinting(Element el) throws InvalidXMLException { return new PoseFilter(PoseFlag.SPRINTING); } @MethodParser("gliding") public Filter parseGlidingFilter(Element el) throws InvalidXMLException { return new PoseFilter(PoseFlag.GLIDING); } @MethodParser("flying") public Filter parseFlying(Element el) throws InvalidXMLException { return new PoseFilter(PoseFlag.FLYING); } @MethodParser("can-fly") public CanFlyFilter parseCanFly(Element el) throws InvalidXMLException { return new CanFlyFilter(); } private Filter parseExplicitTeam(Element el, CompetitorFilter filter) throws InvalidXMLException { final boolean any = XMLUtils.parseBoolean(el.getAttribute("any"), false); final Optional<TeamFactory> team = teamParser.property(el).optional(); if(any && team.isPresent()) { throw new InvalidXMLException("Cannot combine attributes 'team' and 'any'", el); } return any || team.isPresent() ? new TeamFilterAdapter(team, filter) : filter; } private GoalDefinition goalReference(Element el) throws InvalidXMLException { return features.reference(new Node(el), GoalDefinition.class); } private GoalFilter goalFilter(Element el) throws InvalidXMLException { return new GoalFilter(goalReference(el)); } @MethodParser("objective") public Filter parseGoal(Element el) throws InvalidXMLException { return parseExplicitTeam(el, goalFilter(el)); } @MethodParser("completed") public Filter parseCompleted(Element el) throws InvalidXMLException { return new TeamFilterAdapter(Optional.empty(), goalFilter(el)); } @MethodParser("captured") public Filter parseCaptured(Element el) throws InvalidXMLException { final GoalFilter goal = goalFilter(el); final Optional<TeamFactory> team = teamParser.property(el).optional(); return team.isPresent() ? new TeamFilterAdapter(team, goal) : goal; } @MethodParser("rank") public Filter parseRankFilter(Element el) throws InvalidXMLException { return parseExplicitTeam(el, new RankFilter(XMLUtils.parseNumericRange(new Node(el), Integer.class))); } @MethodParser("score") public Filter parseScoreFilter(Element el) throws InvalidXMLException { return parseExplicitTeam(el, new ScoreFilter(XMLUtils.parseNumericRange(new Node(el), Integer.class))); } protected FlagStateFilter parseFlagState(Element el, Class<? extends State> state) throws InvalidXMLException { return new FlagStateFilter(features.reference(new Node(el), FlagDefinition.class), Node.tryAttr(el, "post").map(rethrowFunction(attr -> features.reference(attr, Post.class))), state); } @MethodParser("flag-carried") public FlagStateFilter parseFlagCarried(Element el) throws InvalidXMLException { return this.parseFlagState(el, Carried.class); } @MethodParser("flag-dropped") public FlagStateFilter parseFlagDropped(Element el) throws InvalidXMLException { return this.parseFlagState(el, Dropped.class); } @MethodParser("flag-returned") public FlagStateFilter parseFlagReturned(Element el) throws InvalidXMLException { return this.parseFlagState(el, Returned.class); } @MethodParser("flag-captured") public FlagStateFilter parseFlagCaptured(Element el) throws InvalidXMLException { return this.parseFlagState(el, Captured.class); } @MethodParser("carrying-flag") public CarryingFlagFilter parseCarryingFlag(Element el) throws InvalidXMLException { return new CarryingFlagFilter(features.reference(new Node(el), FlagDefinition.class)); } @MethodParser("cause") public CauseFilter parseCause(Element el) throws InvalidXMLException { return causeFilters.create(XMLUtils.parseEnum(el, CauseFilter.Cause.class, "cause filter")); } @MethodParser("relation") public RelationFilter parseRelation(Element el) throws InvalidXMLException { return new RelationFilter(XMLUtils.parseEnum(el, PlayerRelation.class, "player relation filter")); } private ImItemStack parseFilterItem(Element el) throws InvalidXMLException { return itemModifier.modify(itemParser.parseRequiredItem(el)).immutableCopy(); } @MethodParser("carrying") public CarryingItemFilter parseHasItem(Element el) throws InvalidXMLException { return new CarryingItemFilter(parseFilterItem(el)); } @MethodParser("holding") public HoldingItemFilter parseHolding(Element el) throws InvalidXMLException { return new HoldingItemFilter(parseFilterItem(el)); } @MethodParser("wearing") public WearingItemFilter parseWearingItem(Element el) throws InvalidXMLException { return new WearingItemFilter(parseFilterItem(el)); } @MethodParser("structural-load") public StructuralLoadFilter parseStructuralLoad(Element el) throws InvalidXMLException { return new StructuralLoadFilter(XMLUtils.parseNumber(el, Integer.class)); } @MethodParser("time") public Filter parseTimeFilter(Element el) throws InvalidXMLException { final Duration duration = XMLUtils.parseDuration(el, (Duration) null); if(Comparables.greaterThan(duration, Duration.ZERO)) { return new AllFilter( MatchStateFilter.started(), new MonostableFilter( duration, MatchStateFilter.running(), Optional.empty() ).not() ); } else { return new MatchStateFilter(MatchState.Running, MatchState.Finished); } } @MethodParser("countdown") public Filter parseCountdownFilter(Element el) throws InvalidXMLException { final Duration duration = XMLUtils.parseDuration(el, "duration").required(); if(Comparables.greaterThan(duration, Duration.ZERO)) { return new MonostableFilter(duration, filterParser.parseReferenceOrChild(el), messageTemplates.property(el, "message") .placeholders(Range.closed(0, 1)) .optional()); } else { return new StaticFilter(Filter.QueryResponse.DENY); } } @MethodParser("mutation") public MatchMutationFilter parseMatchMutation(Element el) throws InvalidXMLException { return new MatchMutationFilter(XMLUtils.parseEnum(el, Mutation.class, "match mutation")); } @MethodParser("participating") public Filter parseParticipating(Element el) throws InvalidXMLException { return new ParticipatingFilter(true); } @MethodParser("observing") public Filter parseObserving(Element el) throws InvalidXMLException { return new ParticipatingFilter(false); } @MethodParser("match-started") public Filter parseMatchStarted(Element el) throws InvalidXMLException { return new MatchStateFilter(MatchState.Running, MatchState.Finished); } @MethodParser("match-running") public Filter parseMatchRunning(Element el) throws InvalidXMLException { return new MatchStateFilter(MatchState.Running); } @MethodParser("match-finished") public Filter parseMatchFinished(Element el) throws InvalidXMLException { return new MatchStateFilter(MatchState.Finished); } @MethodParser("players") public Filter parsePlayerCount(Element el) throws InvalidXMLException { return new PlayerCountFilter(filterParser.parseReferenceOrChild(el), XMLUtils.parseNumericRange(el, Integer.class, Range.atLeast(1)), XMLUtils.parseBoolean(el, "participants").optional(true), XMLUtils.parseBoolean(el, "observers").optional(false)); } @MethodParser public Filter attribute(Element el) throws InvalidXMLException { final Node node = Node.of(el); return new AttributeFilter(attributeParser.parse(node), XMLUtils.parseNumericRange(el, Double.class)); } }