package com.asteria.task;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
import java.util.Queue;
import com.asteria.game.World;
import com.google.common.base.Preconditions;
/**
* A sequence of {@link LinkedTask}s that are ran in <i>FIFO</i> order, and the
* solution to scheduling tasks in bulk. The problem with scheduling many tasks
* in succession is that math (a pain) or nesting tasks (very ugly) will have to
* be used in order to time the delays properly. Another issue is having 10s,
* 100s, and in some cases maybe even 1000s of tasks running at once for one
* single context!
* <p>
* <p>
* Linked task sequences work by utilizing an internal queue to keep a record of
* tasks, in the order they were added. They can be configured to {@code replay}
* so that when the sequence finishes, it starts over again. When a sequence is
* executed, it acts as a single task determining when links need to be executed
* (and subsequently removed) which solves the issue of having too many tasks
* running for a single context. The way that execution for links is done is in
* succession, meaning this solves this issue of having to nest tasks or use
* math to figure out the delays; For instance, take these examples:
*
* <pre>
* World.submit(new Task(5, false) {
* @Override
* public void execute() {
* System.out.println("#1: executed after 5 ticks");
* }
* });
*
* World.submit(new Task(7, false) {
* @Override
* public void execute() {
* System.out.println("#2: executed 2 ticks after #1");
* }
* });
* </pre>
*
* Or instead of the math, the nesting tasks alternative.
*
* <pre>
* World.submit(new Task(5, false) {
* @Override
* public void execute() {
* System.out.println("#1: executed after 5 ticks");
*
* World.submit(new Task(2, false) {
* @Override
* public void execute() {
* System.out.println("#2: executed 2 ticks after #1");
* }
* });
* }
* })
* </pre>
*
* Both very ugly, and extremely verbose. Now the same thing with a linked task
* sequence...
*
* <pre>
* LinkedTaskSequence queue = new LinkedTaskSequence();
* queue.connect(5, () -> System.out.println("#1: executed after 5 ticks"));
* queue.connect(2, () -> System.out.println("#2: executed 2 ticks after #1"));
* queue.start();
* </pre>
*
* @author lare96 <http://github.org/lare96>
*/
public final class LinkedTaskSequence extends Task {
/**
* The queue that contains a cache of the connected links. This cache is
* maintained in order to ensure that the original connection of links is
* never lost no matter how many times the sequence is executed. It can be
* modified while this sequence is running, although links will not be added
* to the main queue until the next sequence or replay.
*/
private final Queue<LinkedTask> linkCache = new ArrayDeque<>();
/**
* The main queue that is actually modified when this sequence is ran. If
* links are added to the cache when this sequence is running, they will not
* be added to this queue until the next sequence or replay.
*/
private final Queue<LinkedTask> linkQueue = new ArrayDeque<>();
/**
* The flag that determines if this sequence should replay; as in, if this
* connection should keep on being executed once it completes.
*/
private final boolean replay;
/**
* The counter that determines when each individual link will be ran.
*/
private int tickCounter = -1;
/**
* Creates a new {@link LinkedTaskSequence}.
*
* @param replay
* determines if this sequence should replay.
*/
public LinkedTaskSequence(boolean replay) {
super(1, false);
this.replay = replay;
}
/**
* Creates a new {@link LinkedTaskSequence} that does not replay upon
* completion.
*/
public LinkedTaskSequence() {
this(false);
}
@Override
public void onSubmit() {
Preconditions.checkState(linkCache.size() > 0, "linkCache.size() == 0");
Preconditions.checkState(tickCounter == -1, "tickCounter != -1");
tickCounter = 0;
linkQueue.addAll(linkCache);
}
@Override
public void onSequence() {
if (linkQueue.peek() == null && replay)
linkQueue.addAll(linkCache);
}
@Override
public void execute() {
LinkedTask link = linkQueue.peek();
if (link == null) {
this.cancel();
return;
}
if (++tickCounter == link.getDelay()) {
try {
link.execute();
} finally {
linkQueue.poll();
tickCounter = 0;
}
}
}
@Override
public void onCancel() {
if (linkQueue.size() > 0)
linkQueue.clear();
tickCounter = -1;
}
/**
* Starts this sequence by submitting itself as a task. The equivalent to:
* <p>
* <p>
* {@code World.submit(this);}
* <p>
* This is just an easier alternative.
*
* @return an instance of itself, for chaining.
*/
public LinkedTaskSequence start() {
World.submit(this);
return this;
}
/**
* Clears all of the links in the backing link cache.
*
* @return an instance of itself, for chaining.
*/
public LinkedTaskSequence clear() {
linkCache.clear();
return this;
}
/**
* Connects {@code link} to this sequence.
*
* @param link
* the link to connect, cannot be {@code null}.
* @return an instance of itself, for chaining.
*/
public LinkedTaskSequence connect(LinkedTask link) {
linkCache.add(Objects.requireNonNull(link));
return this;
}
/**
* Connects a link to this sequence. The difference between this function
* and {@code connect(LinkedTask)} is that this function is designed for
* very small links that do not need their own full fledged linked task; for
* instance, take this verbose code:
*
* <pre>
* connect(new LinkedTask(2) {
* @Override
* public void execute() {
* System.out.println("very small link");
* }
* });
* </pre>
*
* We can use this method, since the link only has one small operation. This
* method will convert the code into the above anyway under-the-hood, making
* things less verbose.
*
* <pre>
* connect(2, () -> System.out.println("very small link"));
* </pre>
*
*
* @param link
* the link to connect, cannot be {@code null}.
* @return an instance of itself, for chaining.
*/
public LinkedTaskSequence connect(int delay, Runnable task) {
connect(new LinkedTask(delay) {
@Override
public void execute() {
task.run();
}
});
return this;
}
/**
* Connects a series of links to this sequence.
*
* @param links
* the links to connect.
* @return an instance of itself, for chaining.
*/
public LinkedTaskSequence connectAll(Collection<LinkedTask> links) {
links.forEach(this::connect);
return this;
}
/**
* Connects a series of links to this sequence.
*
* @param links
* the links to connect.
* @return an instance of itself, for chaining.
*/
public LinkedTaskSequence connectAll(LinkedTask... links) {
connectAll(Arrays.asList(links));
return this;
}
}