package com.badlogic.gdx.automation.recorder;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.automation.recorder.InputProperty.SyncProperty;
import com.badlogic.gdx.automation.recorder.InputProperty.SyncProperty.Accelerometer;
import com.badlogic.gdx.automation.recorder.InputProperty.SyncProperty.Button;
import com.badlogic.gdx.automation.recorder.InputProperty.SyncProperty.KeyEvent;
import com.badlogic.gdx.automation.recorder.InputProperty.SyncProperty.KeyPressed;
import com.badlogic.gdx.automation.recorder.InputProperty.SyncProperty.Orientation;
import com.badlogic.gdx.automation.recorder.InputProperty.SyncProperty.Pointer;
import com.badlogic.gdx.automation.recorder.InputProperty.SyncProperty.PointerEvent;
import com.badlogic.gdx.automation.recorder.InputProperty.SyncPropertyVisitor;
import com.badlogic.gdx.automation.recorder.io.InputRecordReader;
/**
* Standard implementation of a player playing back recorded or generated input.
*
* @author Lukas Böhm
*/
public class InputPlayer {
private final PlaybackInput playback;
private final List<PlaybackListener> listeners;
private final MainThreadRunnable mainThread;
private final ReaderThreadRunnable readerThread;
private final RecordProperties properties;
/**
* the original input provided by the libGdx back end in use
*/
private final Iterator<SyncProperty> syncIterator;
public static final String LOG_TAG = "InputPlayer";
public InputPlayer(InputRecordReader reader) {
properties = reader.getRecordProperties();
playback = new PlaybackInput(reader.getTextIterator(),
reader.getPlaceholderTextIterator(), reader.getStaticValues());
listeners = new ArrayList<PlaybackListener>();
syncIterator = reader.getSyncValueIterator();
mainThread = new MainThreadRunnable();
readerThread = new ReaderThreadRunnable();
}
public void startPlayback() {
synchronized (Gdx.input) {
Input gdxInput = Gdx.input;
playback.setProxiedInput(Gdx.input);
Gdx.input = playback;
playback.setInputProcessor(gdxInput.getInputProcessor());
gdxInput.setInputProcessor(null);
}
readerThread.start();
mainThread.start();
notifyStart();
}
/**
* Delays the playback of the recorded input by the given amount of time (in
* seconds).
*
* @param seconds
*/
public void playbackTimeout(float seconds) {
readerThread.delay((int) (seconds * 1000));
}
public void stopPlayback() {
mainThread.stop();
readerThread.stop();
if (!InputProxy.removeProxyFromGdx(playback)) {
Gdx.app.log(LOG_TAG, "Could not remove player from Gdx.input");
}
Gdx.input.setInputProcessor(playback.getInputProcessor());
notifyStopped();
}
public void addPlaybackListener(PlaybackListener listener) {
listeners.add(listener);
}
public void removePlaybackListener(PlaybackListener listener) {
listeners.remove(listener);
}
public void clearPlaybackListeners() {
listeners.clear();
}
private void notifyStopped() {
for (PlaybackListener listener : listeners) {
listener.onStop();
}
}
private void notifyStart() {
for (PlaybackListener listener : listeners) {
listener.onStart();
}
}
private void notifyFinished() {
Gdx.app.postRunnable(new Runnable() {
@Override
public void run() {
for (PlaybackListener listener : listeners) {
listener.onSyncPropertiesFinish();
}
}
});
}
/**
* Hooks into the application's main thread/loop to notify the current
* {@link InputProcessor} about key and touch events as any other backend
* would do.
*
*/
private class MainThreadRunnable implements Runnable {
private boolean interrupted = true;
private boolean stopped = true;
public synchronized void start() {
if (interrupted) {
// busy waiting
while (!stopped)
continue;
interrupted = false;
Gdx.app.postRunnable(this);
stopped = false;
}
}
public synchronized void stop() {
interrupted = true;
}
@Override
public void run() {
if (!interrupted) {
playback.processEvents();
Gdx.app.postRunnable(this);
} else {
stopped = true;
}
}
}
/**
* A separate thread to apply {@link InputProperty} changes read from the
* {@link InputRecordReader} depending on time.
*
* @author Lukas Böhm
*
*/
private class ReaderThreadRunnable implements Runnable {
/**
* Defines how long the reader thread will at least sleep after a
* sequence of events has been processed. It can also be interpreted as
* the number of milliseconds that can lie between two events so that
* those events are still considered as having happened at the same
* time.
*/
private static final int MIN_SLEEP = 10;
private Thread thread = null;
private int delayMs = 0;
public synchronized void start() {
if (thread != null && thread.isAlive()) {
if (thread.isInterrupted()) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
return;
}
}
thread = new Thread(this);
thread.setDaemon(true);
thread.start();
}
public void delay(int ms) {
// TODO care about ms < 0?
synchronized (thread) {
delayMs += ms;
}
}
public synchronized void stop() {
if (thread != null) {
thread.interrupt();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void run() {
if (properties.absouluteCoords) {
denormalizedProcessing();
} else {
normalizedProcessing();
}
}
private void denormalizedProcessing() {
int sleep;
SyncProperty currentVal;
InputState state = playback.getState();
while (!Thread.currentThread().isInterrupted()) {
sleep = 0;
do {
if (delayMs != 0) {
break;
}
if (!syncIterator.hasNext()) {
notifyFinished();
return;
}
currentVal = syncIterator.next();
sleep += currentVal.timeDelta;
synchronized (state) {
state.apply(currentVal);
}
} while (sleep <= MIN_SLEEP);
if (delayMs != 0) {
synchronized (thread) {
sleep += delayMs;
delayMs = 0;
}
}
try {
Thread.sleep(sleep);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
break;
}
}
}
private void normalizedProcessing() {
int sleep;
SyncProperty currentVal;
InputState state = playback.getState();
while (!Thread.currentThread().isInterrupted()) {
sleep = 0;
do {
if (delayMs != 0) {
break;
}
if (!syncIterator.hasNext()) {
notifyFinished();
return;
}
currentVal = syncIterator.next();
PropertyDenormalizer.denormalize(currentVal);
sleep += currentVal.timeDelta;
synchronized (state) {
state.apply(currentVal);
}
} while (sleep <= MIN_SLEEP);
if (delayMs != 0) {
synchronized (thread) {
sleep += delayMs;
delayMs = 0;
}
}
try {
Thread.sleep(sleep);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
break;
}
}
}
}
/**
* A class providing static functionality to denormalize screen coordinates
* and deltas that are part of {@link SyncProperty SyncProperties}. This
* mainly affects {@link Pointer} and {@link PointerEvent};
*
* @author Lukas Böhm
*
*/
public static class PropertyDenormalizer implements SyncPropertyVisitor {
private static PropertyDenormalizer denormalizer;;
private PropertyDenormalizer() {
}
public static void denormalize(SyncProperty property) {
if (denormalizer == null) {
denormalizer = new PropertyDenormalizer();
}
property.accept(denormalizer);
}
@Override
public void visitAccelerometer(Accelerometer accelerometer) {
}
@Override
public void visitKeyPressed(KeyPressed keyPressed) {
}
@Override
public void visitPointerEvent(PointerEvent pointerEvent) {
int width = Gdx.graphics.getWidth();
int height = Gdx.graphics.getHeight();
pointerEvent.x *= width;
pointerEvent.y *= height;
}
@Override
public void visitKeyEvent(KeyEvent keyEvent) {
}
@Override
public void visitOrientation(Orientation orientation) {
}
@Override
public void visitPointer(Pointer pointer) {
int width = Gdx.graphics.getWidth();
int height = Gdx.graphics.getHeight();
pointer.x *= width;
pointer.deltaX *= width;
pointer.y *= height;
pointer.deltaY *= height;
}
@Override
public void visitButton(Button button) {
}
}
}