299 lines
9.9 KiB
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();
|
|
}
|
|
}
|
|
}
|