ProjectAres/PGM/src/main/java/tc/oc/pgm/countdowns/CountdownContext.java

299 lines
9.9 KiB
Java

package tc.oc.pgm.countdowns;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import javax.inject.Inject;
import org.bukkit.event.EventException;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.server.ServerSuspendEvent;
import tc.oc.commons.core.IterableUtils;
import tc.oc.commons.core.concurrent.SerializingExecutor;
import tc.oc.commons.core.logging.Loggers;
import tc.oc.commons.core.scheduler.Scheduler;
import tc.oc.commons.core.scheduler.Task;
import tc.oc.commons.core.util.Comparables;
import tc.oc.commons.core.util.Predicates;
import tc.oc.commons.core.util.Streams;
import tc.oc.commons.core.util.TimeUtils;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.match.MatchScope;
import tc.oc.pgm.time.TickClock;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
@ListenerScope(MatchScope.LOADED)
public abstract class CountdownContext implements Listener {
public static final Duration MIN_REPEAT_INTERVAL = Duration.ofMillis(50);
protected Logger logger;
@Inject void setLogger(Loggers loggers) { logger = loggers.get(getClass()); }
@Inject protected Scheduler scheduler;
@Inject protected TickClock clock;
/**
* Must allow concurrent modification
*/
protected abstract Stream<Runner> runners();
protected abstract Optional<Runner> runner(Countdown countdown);
protected abstract void addRunner(Runner runner);
protected abstract void removeRunner(Runner runner);
public abstract Set<Countdown> getAll();
public void start(Countdown countdown, int seconds) {
start(countdown, Duration.ofSeconds(seconds));
}
public void start(Countdown countdown, Duration duration) {
start(countdown, duration, duration);
}
public void start(Countdown countdown, Duration initialDuration, @Nullable Duration repeatDuration) {
start(countdown, initialDuration, repeatDuration, 1);
}
/**
* Start running the given {@link Countdown} in this context. If the given Countdown
* is already running, it will be cancelled and restarted with the given duration.
*/
public void start(Countdown countdown, Duration initialDuration, @Nullable Duration repeatDuration, int repeatCount) {
cancel(countdown);
new Runner(countdown, initialDuration, repeatDuration != null ? repeatDuration : initialDuration, repeatCount);
}
public void cancel(Countdown countdown) {
runner(countdown).ifPresent(r -> r.cancel(false));
}
public Stream<Countdown> countdowns() {
return getAll().stream();
}
public <T extends Countdown> Stream<T> countdowns(Class<T> type) {
return Streams.instancesOf(countdowns(), type);
}
public <T extends Countdown> Set<T> getAll(Class<T> type) {
return IterableUtils.instancesOf(getAll(), type);
}
public @Nullable Duration getTimeLeft(Countdown countdown) {
return runner(countdown).map(runner -> Duration.ofSeconds(runner.secondsRemaining))
.orElse(null);
}
public boolean isRunning(Countdown countdown) {
return runner(countdown).filter(runner -> runner.secondsRemaining > 0)
.isPresent();
}
public boolean anyRunning() {
return anyRunning(Predicates.alwaysTrue());
}
public boolean anyRunning(Predicate<? super Countdown> test) {
return countdowns().anyMatch(test);
}
public boolean anyRunning(Class<? extends Countdown> countdownClass) {
return anyRunning(countdown -> countdown.getClass().equals(countdownClass));
}
public void cancelAll() {
cancelAll(false);
}
public void cancelAll(boolean manual) {
if(anyRunning()) {
logger.fine("Cancelling all countdowns");
runners().forEach(runner -> runner.cancel(manual));
}
}
/**
* Cancel all countdowns of the given type
* @return true if any countdowns were cancelled
*/
public boolean cancelAll(Class<? extends Countdown> type) {
if(anyRunning(type::isInstance)) {
logger.fine("Cancelling all " + type.getSimpleName() + " countdowns");
runners().filter(r -> type.isInstance(r.countdown))
.forEach(r -> r.cancel(false));
return true;
}
return false;
}
/**
* Cancel all countdowns that are not of the given type
* @return true if any countdowns were cancelled
*/
public boolean continueAll(Class<? extends Countdown> type) {
if(anyRunning(c -> !type.isInstance(c))) {
logger.fine("Cancelling all countdowns except " + type.getSimpleName());
runners().filter(r -> !type.isInstance(r.countdown))
.forEach(r -> r.cancel(false));
return true;
}
return false;
}
@EventHandler
void suspend(ServerSuspendEvent event) throws EventException {
try { event.yield(); }
finally {
runners().forEach(Runner::resume);
}
}
protected class Runner implements Runnable {
protected final Countdown countdown;
protected final Duration initialDuration;
protected final Duration repeatDuration;
private final int repeatCount;
private int count;
private Duration duration;
private Instant startedAt;
private Instant willEndAt;
// The remaining seconds that will be passed to onTick for the next cycle
private long secondsRemaining;
// Update task, replaced on every update
private @Nullable Task task = null;
// Ensures that only one callback is executing at a time for this countdown.
// So, e.g. if the onStart callback cancels the countdown, onCancel won't
// run until onStart returns.
private final SerializingExecutor notifyExecutor = new SerializingExecutor();
Runner(Countdown countdown, Duration initialDuration, Duration repeatDuration, int repeatCount) {
checkArgument(!(repeatCount > 1 && Comparables.lessThan(repeatDuration, MIN_REPEAT_INTERVAL)));
this.countdown = checkNotNull(countdown);
this.initialDuration = checkNotNull(initialDuration);
this.repeatDuration = checkNotNull(repeatDuration);
this.repeatCount = repeatCount;
restart();
}
void restart() {
++count;
if(repeatCount == Integer.MAX_VALUE || count <= repeatCount) {
duration = count == 1 ? initialDuration : repeatDuration;
startedAt = clock.now().instant;
if(TimeUtils.isInfPositive(duration)) {
willEndAt = TimeUtils.INF_FUTURE;
secondsRemaining = Long.MAX_VALUE;
} else {
willEndAt = startedAt.plus(duration);
secondsRemaining = duration.getSeconds();
task = scheduler.createTask(this);
}
if(logger.isLoggable(Level.FINE)) {
logger.fine("Starting countdown " + countdown +
" at " + startedAt +
" for " + duration);
}
addRunner(this);
notify(() -> countdown.onStart(duration, duration));
}
}
protected Countdown countdown() {
return countdown;
}
protected Duration remaining() {
return TimeUtils.duration(clock.now().instant, willEndAt);
}
protected void cancel(boolean manual) {
logger.fine("Cancelling countdown " + countdown);
final Duration remaining = TimeUtils.duration(clock.now().instant, willEndAt);
if(task != null) {
task.cancel();
task = null;
}
removeRunner(this);
notify(() -> countdown.onCancel(remaining, duration, manual));
}
@Override
public void run() {
if(task == null) return;
task = null;
if(secondsRemaining < 0) return;
// Get the total ticks remaining in the countdown
long ticksRemaining = Math.round(remaining().toMillis() / 50d);
// Handle any cycles since the last one
for(;secondsRemaining >= 0 && secondsRemaining * 20 >= ticksRemaining; secondsRemaining--) {
countdown.onTick(Duration.ofSeconds(secondsRemaining), duration);
}
if(secondsRemaining >= 0) {
// If there are cycles left, schedule the next run
long ticks = ticksRemaining - secondsRemaining * 20;
task = scheduler.createDelayedTask(ticks < 1 ? 1 : ticks, this);
} else {
// Otherwise, end the countdown
logger.fine("Ending countdown " + countdown);
secondsRemaining = 0;
// Remove from context before calling onEnd, so if it starts another
// countdown, it won't try to cancel this one.
if(count >= repeatCount) {
removeRunner(this);
}
notify(() -> countdown.onEnd(duration));
restart();
}
}
void notify(Runnable callback) {
try {
notifyExecutor.execute(callback);
} catch(Throwable e) {
logger.log(Level.SEVERE, "Exception notifying countdown " + countdown, e);
}
}
void resume() {
// Skip all the ticks that happened while suspended
secondsRemaining = remaining().getSeconds();
}
}
}