package tc.oc.pgm.match; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import javax.inject.Inject; import com.google.common.util.concurrent.ListeningScheduledExecutorService; import com.google.common.util.concurrent.MoreExecutors; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import tc.oc.minecraft.scheduler.MainThreadExecutor; import tc.oc.commons.core.util.Comparables; import tc.oc.pgm.events.ListenerScope; import tc.oc.pgm.events.MatchBeginEvent; import tc.oc.pgm.events.MatchEndEvent; import tc.oc.pgm.events.MatchUnloadEvent; /** * Can be used by other modules to schedule tasks in real time (not tick time). * * Tasks scheduled with zero delay are run synchronously. * * Pending tasks are cancelled when the match unloads, and tasks that are scoped * to the running match are cancelled when the match ends. */ @ListenerScope(MatchScope.LOADED) public class MatchRealtimeScheduler implements Listener { private final Match match; private final MainThreadExecutor mainThreadExecutor; private final ListeningScheduledExecutorService schedulerLoaded; private final ListeningScheduledExecutorService schedulerRunning; private final List<Runnable> atMatchStart = new ArrayList<>(); @Inject MatchRealtimeScheduler(Match match, MainThreadExecutor mainThreadExecutor) { this.match = match; this.mainThreadExecutor = mainThreadExecutor; this.schedulerLoaded = newScheduler(); this.schedulerRunning = newScheduler(); } private static ListeningScheduledExecutorService newScheduler() { final ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1); scheduler.setContinueExistingPeriodicTasksAfterShutdownPolicy(false); scheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); scheduler.setRemoveOnCancelPolicy(true); return MoreExecutors.listeningDecorator(scheduler); } public void schedule(Duration delay, Runnable task) { if(Comparables.greaterThan(delay, Duration.ZERO)) { schedulerLoaded.schedule(() -> schedule(Duration.ZERO, task), delay.toMillis(), TimeUnit.MILLISECONDS); } else { mainThreadExecutor.execute(() -> { if(match.isLoaded()) { task.run(); } }); } } public void schedule(Instant time, Runnable task) { mainThreadExecutor.execute(() -> { if(match.isUnloaded()) return; final Instant now = match.getInstantNow(); if(!match.isLoaded() || time.isAfter(now)) { schedulerLoaded.schedule(() -> schedule(time, task), Math.max(50, Duration.between(now, time).toMillis()), TimeUnit.MILLISECONDS); } else { task.run(); } }); } public void scheduleAtRunningTime(Duration time, Runnable task) { mainThreadExecutor.execute(() -> { if(!match.hasStarted()) { atMatchStart.add(() -> scheduleAtRunningTime(time, task)); } else if(!match.isFinished()) { final Duration now = match.runningTime(); if(Comparables.greaterThan(time, now)) { schedulerRunning.schedule(() -> scheduleAtRunningTime(time, task), Math.max(50, time.toMillis() - now.toMillis()), TimeUnit.MILLISECONDS); } else { task.run(); } } }); } @EventHandler void start(MatchBeginEvent event) { atMatchStart.forEach(mainThreadExecutor::execute); atMatchStart.clear(); } @EventHandler void start(MatchEndEvent event) { schedulerRunning.shutdown(); } @EventHandler void unload(MatchUnloadEvent event) { schedulerLoaded.shutdown(); } }