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.
This commit is contained in:
garbagemule 2014-02-25 01:09:03 +01:00
parent f85f9e20d7
commit a95ab27d5e
6 changed files with 524 additions and 0 deletions

View File

@ -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;
}
}

View File

@ -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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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);
}
}
}

View File

@ -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.
* <p>
* Every time the tick interval has passed, the {@code onTick()} method on the
* underlying {@link TimerCallback} is called.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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);
}
}
}

View File

@ -0,0 +1,86 @@
package com.garbagemule.MobArena.util.timer;
/**
* Generic Timer interface for various implementations of timers.
* <p>
* 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.
* <p>
* 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.
* <p>
* Conditions for callbacks:
* <ul>
* <li>When a timer is started via its {@link #start()} method, it calls
* {@code onStart()} on the callback.
* <li>If a timer is manually stopped, it calls {@code onStop()} on the
* callback when its {@link #stop()} method is called.
* <li>If a timer can "tick", it calls {@code onTick()} on the callback
* every time it ticks.
* <li>If a timer can finish ("run out"), it calls the {@code onFinish()}
* method on the callback when it finishes.
* </ul>
* 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.
* <p>
* 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.
* <p>
* 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);
}

View File

@ -0,0 +1,39 @@
package com.garbagemule.MobArena.util.timer;
public interface TimerCallback {
/**
* Called when the timer is started.
* <p>
* 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.
* <p>
* Ticks are implementation-specific. Refer to the documentation of the
* specific timer for details.
*/
public void onTick();
/**
* Called when the timer finishes.
* <p>
* 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.
* <p>
* 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();
}

View File

@ -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() {}
}