package tc.oc.pgm.filters.matcher.match; import java.time.Duration; import java.time.Instant; import java.util.HashMap; import java.util.Map; import java.util.Optional; import net.md_5.bungee.api.ChatColor; import org.bukkit.boss.BarColor; import org.bukkit.entity.Player; import tc.oc.commons.bukkit.localization.MessageTemplate; import tc.oc.time.PeriodConverters; import tc.oc.time.PeriodRenderers; import tc.oc.commons.core.util.Comparables; import tc.oc.commons.core.util.MapUtils; import tc.oc.commons.core.util.TimeUtils; import tc.oc.pgm.bossbar.BossBarMatchModule; import tc.oc.pgm.countdowns.CountdownBossBarSource; import tc.oc.pgm.features.Feature; import tc.oc.pgm.features.FeatureDefinitionContext; import tc.oc.pgm.features.FeatureFactory; import tc.oc.pgm.features.FeatureValidationContext; import tc.oc.pgm.filters.Filter; import tc.oc.pgm.filters.FilterMatchModule; import tc.oc.pgm.filters.Filterable; import tc.oc.pgm.filters.Filterables; import tc.oc.pgm.filters.matcher.TypedFilter; import tc.oc.pgm.filters.operator.SingleFilterFunction; import tc.oc.pgm.filters.parser.DynamicFilterValidation; import tc.oc.pgm.filters.parser.RespondsToQueryValidation; import tc.oc.pgm.filters.query.IMatchQuery; import tc.oc.pgm.match.Match; import tc.oc.pgm.match.MatchPlayer; import tc.oc.pgm.match.Repeatable; import tc.oc.pgm.xml.InvalidXMLException; /** * Filter function that is low when its operand is low, and high only for * a specified time after its operand goes high. */ public class MonostableFilter extends SingleFilterFunction implements TypedFilter<IMatchQuery>, FeatureFactory<MonostableFilter.Reactor<?>> { private final @Inspect Duration duration; private final @Inspect Optional<MessageTemplate> message; private final boolean colons; public MonostableFilter(Duration duration, Filter trigger, Optional<MessageTemplate> message) { super(trigger); this.duration = duration; this.message = message; this.colons = Comparables.greaterOrEqual(duration, Duration.ofSeconds(90)); } public static Filter after(FeatureDefinitionContext context, Duration time) throws InvalidXMLException { return MatchStateFilter.running().and( // Must be in the FDC so load() is called context.define(Filter.class, new MonostableFilter( time, MatchStateFilter.running(), Optional.empty() ) ).not() ); } @Override public void validate(FeatureValidationContext context) throws InvalidXMLException { context.validate(filter, DynamicFilterValidation.INSTANCE); context.validate(filter, RespondsToQueryValidation.get(message.isPresent() ? MatchPlayer.class : Match.class)); } @Override public void load(Match match) { match.feature(this); } @Override public Reactor<?> createFeature(Match match) { return new Reactor<>(match, Filterables.scope(filter)); } @Override public boolean matches(IMatchQuery query) { return query.feature(this).matches(query); } class Reactor<F extends Filterable<?>> implements Feature<MonostableFilter> { final Match match; final FilterMatchModule fmm; final BossBarMatchModule bbmm; final Class<F> scope; final Optional<Bar> bar; // Filterables that currently pass the inner filter, mapped to the instants that they expire. // They are not actually removed until the inner filter goes false. final Map<F, Instant> endTimes = new HashMap<>(); Instant lastTick = TimeUtils.INF_PAST; Reactor(Match match, Class<F> scope) { this.match = match; this.scope = scope; this.bbmm = match.needMatchModule(BossBarMatchModule.class); this.fmm = match.needMatchModule(FilterMatchModule.class); fmm.onChange(scope, filter, this::matches); bar = message.map(Bar::new); bar.ifPresent(bar -> { // If a countdown message is specified, register a global BossBarSource. // Every player will have a view of this source, but it will hide itself // at rendering time for viewers that do not pass the filter. bbmm.add(bar); // Invalidate the bar when this filter (not the inner filter) changes for any player. // It's easier to do this with a seperate listener, rather than trying to do it in the // trigger listener, because that listener may be at a more general scope and won't always // be called when the filter changes for individual players, e.g. if it's party scoped and // a player changes parties. fmm.onChange(MatchPlayer.class, MonostableFilter.this, (player, response) -> { bbmm.invalidate(bar, player.getBukkit()); }); }); } void invalidate(F filterable) { fmm.invalidate(getDefinition(), filterable); } boolean matches(IMatchQuery query) { return query.filterable(scope) .filter(filterable -> matches(filterable, filter.response(query))) .isPresent(); } boolean matches(F filterable, boolean response) { if(response) { final Instant now = match.getInstantNow(); final Instant end = endTimes.computeIfAbsent(filterable, f -> { invalidate(filterable); return now.plus(duration); }); return now.isBefore(end); } else { if(endTimes.remove(filterable) != null) { invalidate(filterable); } return false; } } @Repeatable void tick() { final Instant now = match.getInstantNow(); endTimes.forEach((filterable, end) -> { if(now.isBefore(end)) { // If the entry is still valid, check if its elapsed time crossed a second // boundary over the last tick, and invalidate the boss bar if it did. bar.ifPresent(bar -> { if(Duration.between(lastTick, end).getSeconds() != Duration.between(now, end).getSeconds()) { filterable.filterableDescendants(MatchPlayer.class) .forEach(player -> bbmm.invalidate(bar, player.getBukkit())); } }); } else if(lastTick.isBefore(end)) { // If entry is expired, but was not expired last tick, invalidate this filter invalidate(filterable); } }); lastTick = now; } @Override public MonostableFilter getDefinition() { return MonostableFilter.this; } class Bar extends CountdownBossBarSource { final MessageTemplate message; Bar(MessageTemplate message) { super(duration, ChatColor.YELLOW, ChatColor.AQUA, colons ? PeriodConverters.normalized() : PeriodConverters.seconds(), colons ? PeriodRenderers.colons() : PeriodRenderers.natural()); this.message = message; } @Override protected MessageTemplate barMessage(Player viewer) { return message; } @Override protected Optional<Duration> barTime(Player viewer) { return match.player(viewer) .flatMap(player -> player.filterableAncestor(scope)) .flatMap(filterable -> MapUtils.value(endTimes, filterable)) .flatMap(end -> TimeUtils.positiveDuration(match.getInstantNow(), end)); } @Override public BarColor barColor(Player viewer) { return BarColor.YELLOW; } } } }