From a95ab27d5e290c22d930345182f40fd6a90c13fb Mon Sep 17 00:00:00 2001 From: garbagemule Date: Tue, 25 Feb 2014 01:09:03 +0100 Subject: [PATCH] Add Timer framework. This framework consists of interfaces and classes for managing various kinds of timers. The Timer interface models a timer with configurable tick intervals. The TimerCallback interface provides a means of adding logic to the following timer events: onStart, onStop, onFinish and onTick. Timer implementations include a CountdownTimer (kitchen timer), and a StopwatchTimer, which should both be self-explanatory. The purpose of this framework is to add a means of creating a very generalized approach to timers, in hopes of abstracting away some of the Bukkit scheduling for easy maintenance and increased portability. Future work: - Port auto-start-timer - Port boss abilities - Port spawner - Create 'delay-timer' as per DBO ticket request. - Create various types of cooldowns, delays, etc. --- .../MobArena/util/timer/AbstractTimer.java | 40 ++++ .../MobArena/util/timer/CountdownTimer.java | 211 ++++++++++++++++++ .../MobArena/util/timer/StopwatchTimer.java | 129 +++++++++++ .../MobArena/util/timer/Timer.java | 86 +++++++ .../MobArena/util/timer/TimerCallback.java | 39 ++++ .../util/timer/TimerCallbackAdapter.java | 19 ++ 6 files changed, 524 insertions(+) create mode 100644 src/com/garbagemule/MobArena/util/timer/AbstractTimer.java create mode 100644 src/com/garbagemule/MobArena/util/timer/CountdownTimer.java create mode 100644 src/com/garbagemule/MobArena/util/timer/StopwatchTimer.java create mode 100644 src/com/garbagemule/MobArena/util/timer/Timer.java create mode 100644 src/com/garbagemule/MobArena/util/timer/TimerCallback.java create mode 100644 src/com/garbagemule/MobArena/util/timer/TimerCallbackAdapter.java diff --git a/src/com/garbagemule/MobArena/util/timer/AbstractTimer.java b/src/com/garbagemule/MobArena/util/timer/AbstractTimer.java new file mode 100644 index 0000000..6fa3d07 --- /dev/null +++ b/src/com/garbagemule/MobArena/util/timer/AbstractTimer.java @@ -0,0 +1,40 @@ +package com.garbagemule.MobArena.util.timer; + +import org.bukkit.plugin.Plugin; + +public abstract class AbstractTimer implements Timer { + protected Plugin plugin; + protected TimerCallback callback; + protected long interval; + + public AbstractTimer(Plugin plugin, long interval, TimerCallback callback) { + this.plugin = plugin; + this.callback = callback; + + setInterval(interval); + } + + @Override + public void setCallback(TimerCallback callback) { + if (callback == null) { + throw new IllegalArgumentException("Callback may not be null."); + } + if (this.callback != null) { + throw new IllegalStateException("Timer already has a callback."); + } + this.callback = callback; + } + + @Override + public long getInterval() { + return interval; + } + + @Override + public void setInterval(long interval) { + if (interval <= 0l) { + throw new IllegalArgumentException("Tick interval must be positive."); + } + this.interval = interval; + } +} diff --git a/src/com/garbagemule/MobArena/util/timer/CountdownTimer.java b/src/com/garbagemule/MobArena/util/timer/CountdownTimer.java new file mode 100644 index 0000000..5ddb6aa --- /dev/null +++ b/src/com/garbagemule/MobArena/util/timer/CountdownTimer.java @@ -0,0 +1,211 @@ +package com.garbagemule.MobArena.util.timer; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitTask; + +/** + * A simple implementation of a generic countdown timer, which has an initial + * duration and a tick interval. + *

+ * Every time the tick interval has passed, the {@code onTick()} method on the + * underlying {@link TimerCallback} is called. If the tick interval is equal + * to the timer duration, {@code onTick()} is never called. + *

+ * When the timer "runs out" (when the duration has passed), the timer calls + * the {@code onFinish()} method on the callback. In case that the duration + * is not divisible by the tick interval, the final interval will be shorter + * than the previous intervals to make sure the timer ends no later than it + * should. + *

+ * Stopping the timer prematurely via the {@link #stop()} method causes the + * timer to immediately call the {@code onStop()} method on the callback, + * and any subsequent ticks will be ignored. The timer supports stopping and + * (re)starting in the same tick. + */ +public class CountdownTimer extends AbstractTimer { + private long duration; + private long remaining; + + private Timer timer; + + /** + * Create a CountdownTimer that will call the {@code onTick()} method on + * the callback every {@code interval} ticks. Furthermore, the timer will + * either call the {@code onFinish()} method on the callback when it ends, + * or the {@code onStop()} method if the timer is stopped prematurely. + * + * @param plugin the plugin responsible for the timer + * @param duration the duration of the timer; must be non-negative + * @param interval the amount of ticks between each {@code onTick()} call + * on the callback object; must be positive and less than + * or equal to {@code duration} + * @param callback a callback object + */ + public CountdownTimer(Plugin plugin, long duration, long interval, TimerCallback callback) { + super(plugin, interval, callback); + + if (duration < 0l) { + throw new IllegalArgumentException("Duration must be non-negative."); + } + this.duration = duration; + this.remaining = 0l; + this.timer = null; + } + + /** + * Create a CountdownTimer with the given duration and tick interval. + *

+ * This constructor leaves the timer in an inconsistent state until the + * {@link #setCallback(TimerCallback)} method is called with a valid + * callback object. + * + * @param plugin the plugin responsible for the timer + * @param duration the duration of the timer; must be non-negative + * @param interval the amount of ticks between each {@code onTick()} call + * on the callback object; must be positive and less than + * or equal to {@code duration} + */ + public CountdownTimer(Plugin plugin, long duration, long interval) { + this(plugin, duration, interval, null); + } + + /** + * Create a CountdownTimer that will never tick. The timer will either + * call the {@code onFinish()} method on the callback when it ends, or + * the {@code onStop()} method if the timer is stopped prematurely. + * + * @param plugin the plugin responsible for the timer + * @param duration the duration of the timer; must be non-negative + * @param callback a callback object + */ + public CountdownTimer(Plugin plugin, long duration, TimerCallback callback) { + this(plugin, duration, duration, callback); + } + + /** + * Create a CountdownTimer that will never tick. The timer will either + * call the {@code onFinish()} method on the callback when it ends, or + * the {@code onStop()} method if the timer is stopped prematurely. + *

+ * This constructor leaves the timer in an inconsistent state until the + * {@link #setCallback(TimerCallback)} method is called with a valid + * callback object. + * + * @param plugin the plugin responsible for the timer + * @param duration the duration of the timer; must be non-negative + */ + public CountdownTimer(Plugin plugin, long duration) { + this(plugin, duration, duration, null); + } + + /** + * Start the timer. + *

+ * The timer will start counting down from the duration specified in the + * constructor, and count down {@code interval} ticks every time it ticks, + * as well as call the {@code onTick()} method on the callback. + *

+ * If no interval was provided in the constructor, the timer will never + * call the {@code onTick()} method, but only the {@code onFinish()} when + * the timer runs out, or the {@code onStop()} method, if the timer is + * stopped prematurely via the {@link #stop()} method. + */ + @Override + public synchronized void start() { + if (timer != null) { + return; + } + remaining = duration; + callback.onStart(); + timer = new Timer(); + } + + /** + * Stop the timer prematurely. + *

+ * This will call the {@code onStop()} method on the callback, and reset + * the timer to a state in which calling the {@link #start()} method will + * restart the timer. + */ + @Override + public synchronized void stop() { + if (timer == null) { + return; + } + timer.stop(); + timer = null; + remaining = 0l; + callback.onStop(); + } + + /** {@inheritDoc} */ + @Override + public synchronized boolean isRunning() { + return timer != null; + } + + /** + * Get the duration of the timer. + * + * @return the duration of the timer in server ticks + */ + public synchronized long getDuration() { + return duration; + } + + /** + * Get the remaining number of ticks before this timer runs out. + * + * @return the remaining number of server ticks + */ + public synchronized long getRemaining() { + return remaining; + } + + /** + * Internal timer class for the actual legwork. The timer will reschedule + * itself after every tick, if rescheduling is applicable. Furthermore, + * the timer will auto-start on creation to avoid having to schedule it + * from the {@link #start()} method. + */ + private class Timer implements Runnable { + private BukkitTask task; + + public Timer() { + reschedule(); + } + + @Override + public void run() { + synchronized (CountdownTimer.this) { + remaining -= interval; + + // If we're done, call onFinish() and bail + if (remaining <= 0l) { + callback.onFinish(); + return; + } + + // Otherwise, tick + callback.onTick(); + + // If stop() was called from onTick(), don't reschedule + if (task != null) { + reschedule(); + } + } + } + + public synchronized void stop() { + task.cancel(); + task = null; + } + + private synchronized void reschedule() { + // Make sure the timer stops on time + long nextInterval = (remaining < interval) ? remaining : interval; + task = Bukkit.getScheduler().runTaskLater(plugin, this, nextInterval); + } + } +} diff --git a/src/com/garbagemule/MobArena/util/timer/StopwatchTimer.java b/src/com/garbagemule/MobArena/util/timer/StopwatchTimer.java new file mode 100644 index 0000000..0c007cf --- /dev/null +++ b/src/com/garbagemule/MobArena/util/timer/StopwatchTimer.java @@ -0,0 +1,129 @@ +package com.garbagemule.MobArena.util.timer; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitTask; + +/** + * A simple implementation of a generic stopwatch timer, which periodically + * ticks according to a given tick interval. + *

+ * Every time the tick interval has passed, the {@code onTick()} method on the + * underlying {@link TimerCallback} is called. + *

+ * This timer never runs out, and it must be manually stopped via the + * {@link #stop()} method, which causes the timer to immediately call the + * {@code onStop()} method on the callback, and any subsequent ticks will be + * ignored. The timer supports stopping and (re)starting in the same tick. + */ +public class StopwatchTimer extends AbstractTimer { + private Timer timer; + + /** + * Create a StopwatchTimer that will call the {@code onTick()} method on + * the callback every {@code interval} ticks. Furthermore, the timer will + * either call the {@code onFinish()} method on the callback when it ends, + * or the {@code onStop()} method if the timer is stopped prematurely. + * + * @param plugin the plugin responsible for the timer + * @param interval the amount of ticks between each {@code onTick()} call + * on the callback object; must be positive + * @param callback a callback object + */ + public StopwatchTimer(Plugin plugin, long interval, TimerCallback callback) { + super(plugin, interval, callback); + + this.timer = null; + } + + /** + * Create a StopwatchTimer with the given tick interval. + *

+ * This constructor leaves the timer in an inconsistent state until the + * {@link #setCallback(TimerCallback)} method is called with a valid + * callback object. + * + * @param plugin the plugin responsible for the timer + * @param interval the amount of ticks between each {@code onTick()} call + * on the callback object provided later; must be positive + */ + public StopwatchTimer(Plugin plugin, long interval) { + this(plugin, interval, null); + } + + /** + * Start the timer. + *

+ * The timer will start ticking every {@code interval} ticks, as given in + * the constructor, calling the {@code onTick()} method on the callback + * on every tick. + *

+ * The timer will continue ticking until manually stopped via the timer's + * {@link #stop()} method. + */ + @Override + public void start() { + if (timer != null) { + return; + } + callback.onStart(); + timer = new Timer(); + } + + /** + * Stop the timer. + *

+ * This will call the {@code onStop()} method on the callback, and reset + * the timer to a state in which calling the {@link #start()} method will + * restart the timer. + */ + @Override + public void stop() { + if (timer == null) { + return; + } + timer.stop(); + timer = null; + callback.onStop(); + } + + /** {@inheritDoc} */ + @Override + public synchronized boolean isRunning() { + return timer != null; + } + + /** + * Internal timer class for the actual legwork. The timer will reschedule + * itself after every tick, if rescheduling is applicable. Furthermore, + * the timer will auto-start on creation to avoid having to schedule it + * from the {@link #start()} method. + */ + private class Timer implements Runnable { + private BukkitTask task; + + public Timer() { + reschedule(); + } + + @Override + public void run() { + // Tick + callback.onTick(); + + // If stop() was called from onTick(), don't reschedule + if (task != null) { + reschedule(); + } + } + + public synchronized void stop() { + task.cancel(); + task = null; + } + + private synchronized void reschedule() { + task = Bukkit.getScheduler().runTaskLater(plugin, this, interval); + } + } +} diff --git a/src/com/garbagemule/MobArena/util/timer/Timer.java b/src/com/garbagemule/MobArena/util/timer/Timer.java new file mode 100644 index 0000000..017d8b8 --- /dev/null +++ b/src/com/garbagemule/MobArena/util/timer/Timer.java @@ -0,0 +1,86 @@ +package com.garbagemule.MobArena.util.timer; + +/** + * Generic Timer interface for various implementations of timers. + *

+ * The public facing methods of timers provide means of starting and stopping + * the timers, as well as getting and setting tick intervals, and setting + * callbacks post-construction. + *

+ * A timer is expected to call methods on an underlying {@link TimerCallback} + * object (when appropriate), which is provided either in the construction of + * the timer, or via the {@link #setCallback(TimerCallback)} method after the + * timer has been constructed with a null callback. + *

+ * Conditions for callbacks: + *

+ * A timer must support manual stopping, i.e. it must be possible to stop a + * timer, even if it can "run out". Ticking and finishing is optional, as it + * may not always be relevant, depending on the implementation. + */ +public interface Timer { + /** + * Start the timer. + */ + public void start(); + + /** + * Stop the timer. + */ + public void stop(); + + /** + * Check if the timer is running. + * + * @return true, if the timer is currently running, false otherwise + */ + public boolean isRunning(); + + /** + * Set the callback object of the timer. + *

+ * This is a convenience method that allows for creating a callback that + * references the timer. Due to Java's "local variable may not have been + * initialized" rule, an anonymous callback cannot reference its timer + * host, unless the host is a field or a final variable, and creating the + * callback in the same statement as the timer means the timer has not + * yet been initialized, according to Java. As such, if the callback + * references the timer (e.g. to restart it or stop it prematurely), the + * callback must be set with this method after creating the timer with + * a null callback in the constructor. + * + * @param callback a callback object; must be non-null + * @throws IllegalArgumentException if the callback is null + * @throws IllegalStateException if the callback has already been set + */ + public void setCallback(TimerCallback callback); + + /** + * Get the tick interval of the timer. + * + * @return the tick interval of the timer + */ + public long getInterval(); + + /** + * Set the tick interval of the timer. + *

+ * The tick interval may be changed on-the-fly, but will not take effect + * until after the next tick. As such, changing the tick interval after + * the timer has been started without specifying the tick interval in the + * constructor will have no effect. + * + * @param interval tick interval of the timer; must be positive + * @throws IllegalArgumentException if the value is non-positive + */ + public void setInterval(long interval); +} diff --git a/src/com/garbagemule/MobArena/util/timer/TimerCallback.java b/src/com/garbagemule/MobArena/util/timer/TimerCallback.java new file mode 100644 index 0000000..eff3bd4 --- /dev/null +++ b/src/com/garbagemule/MobArena/util/timer/TimerCallback.java @@ -0,0 +1,39 @@ +package com.garbagemule.MobArena.util.timer; + +public interface TimerCallback { + /** + * Called when the timer is started. + *

+ * Note that this method is called before the timer is first scheduled, + * which means the interval can be changed within this method prior to + * the timer actually starting. + */ + public void onStart(); + + /** + * Called when the timer ticks. + *

+ * Ticks are implementation-specific. Refer to the documentation of the + * specific timer for details. + */ + public void onTick(); + + /** + * Called when the timer finishes. + *

+ * A timer finishes when it "runs out", which is implementation-specific. + * For example, the {@link CountdownTimer} finishes when it has counted + * down to 0. + */ + public void onFinish(); + + /** + * Called when the timer is stopped prematurely. + *

+ * Stopping a timer prematurely is different from the timer naturally + * running out, however some timers may never run out and must be + * stopped manually, in which case this method is called instead of + * the {@code onFinish()} method. + */ + public void onStop(); +} diff --git a/src/com/garbagemule/MobArena/util/timer/TimerCallbackAdapter.java b/src/com/garbagemule/MobArena/util/timer/TimerCallbackAdapter.java new file mode 100644 index 0000000..7ac8e75 --- /dev/null +++ b/src/com/garbagemule/MobArena/util/timer/TimerCallbackAdapter.java @@ -0,0 +1,19 @@ +package com.garbagemule.MobArena.util.timer; + +/** + * An empty implementation of the {@link TimerCallback} interface รก la the + * {@link java.awt.event.MouseAdapter} class in the Swing event framwork. + */ +public class TimerCallbackAdapter implements TimerCallback { + @Override + public void onStart() {} + + @Override + public void onTick() {} + + @Override + public void onFinish() {} + + @Override + public void onStop() {} +}