package com.badlogic.gdx.automation.recorder;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.badlogic.gdx.Application;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.automation.recorder.io.InputRecordWriter;
import com.badlogic.gdx.utils.Pool;
import com.badlogic.gdx.utils.ReflectionPool;
/**
* Submodule of {@link InputRecorder} responsible for recording
* {@link InputProperty.SyncProperty}s
*
* @author Lukas Böhm
*
*/
class InputStateTracker {
private final InputRecorder recorder;
private final Processor processor;
/**
* Two arraylists: One to be filled by the grabber and one to be emptied by
* the processor. Then a rapid switch between both can be performed.
*/
private List<InputState> storedStates;
private List<InputState> processedStates;
private final Pool<InputState> statePool;
private final Tracker tracker;
private final InputEventGrabber grabber;
private final GrabberArmer grabberArmer;
private final GrabberKeeper grabberKeeper;
/**
* the flags indicating all values to be tracked on the
* {@link Application#postRunnable(Runnable) main thread}, right before most
* concrete back end {@link Input} implementations call processEvents()
*/
private final int beforePrcessEventsTrackFlags;
/**
* the flags indicating all values to be tracked right at the moment when
* most concrete back end {@link Input} implementations call
* processEvents(), thus being tracked at the perfect moment when the event
* buffers are filled the best
*/
private final int onProcessEventsTrackFlags;
private static final int STATES_UNTIL_PROCESS = 20;
private boolean tracking = false;
InputState currentState;
public InputStateTracker(InputRecorder inputRecorder) {
this.recorder = inputRecorder;
int toSet = 0;
if (recorder.getConfiguration().recordButtons) {
toSet |= InputProperty.SyncProperty.Type.BUTTONS.key;
}
if (recorder.getConfiguration().recordOrientation) {
toSet |= InputProperty.SyncProperty.Type.ORIENTATION.key;
}
if (recorder.getConfiguration().recordKeysPressed) {
toSet |= InputProperty.SyncProperty.Type.KEYS_PRESSED.key;
}
if (recorder.getConfiguration().recordPointers) {
toSet |= InputProperty.SyncProperty.Type.POINTERS.key;
}
beforePrcessEventsTrackFlags = toSet;
toSet = 0;
if (recorder.getConfiguration().recordKeyEvents) {
toSet |= InputProperty.SyncProperty.Type.KEY_EVENTS.key;
}
if (recorder.getConfiguration().recordPointerEvents) {
toSet |= InputProperty.SyncProperty.Type.POINTER_EVENTS.key;
}
onProcessEventsTrackFlags = toSet;
storedStates = new ArrayList<InputState>();
processedStates = new ArrayList<InputState>();
statePool = new ReflectionPool<InputState>(InputState.class, 50);
processor = new Processor();
tracker = new Tracker();
grabber = new InputEventGrabber();
grabberArmer = new GrabberArmer();
grabberKeeper = new GrabberKeeper();
}
public synchronized void startTracking() {
if (isTracking()) {
Gdx.app.log(InputRecorder.LOG_TAG,
"Starting InputStateTracker more than once");
return;
}
tracking = true;
tracker.start();
processor.start();
synchronized (Gdx.input) {
grabberKeeper.setProxiedInput(Gdx.input);
Gdx.input = grabberKeeper;
}
grabberArmer.start();
}
public synchronized void stopTracking() {
if (!isTracking()) {
Gdx.app.log(InputRecorder.LOG_TAG,
"Stopping InputStateTracker more than once");
return;
}
processor.stop();
grabberArmer.stop();
tracker.stop();
InputProxy.removeProxyFromGdx(grabberKeeper);
InputProcessorProxy.removeProxyFromGdxInput(grabber);
tracking = false;
}
public synchronized boolean isTracking() {
return tracking;
}
private void track() {
synchronized (processor) {
synchronized (storedStates) {
currentState = statePool.obtain();
currentState
.initialize(recorder.getConfiguration().recordedPointerCount);
currentState
.set(Gdx.input, beforePrcessEventsTrackFlags, false);
storedStates.add(currentState);
if (storedStates.size() >= STATES_UNTIL_PROCESS) {
processor.notify();
}
}
}
}
/**
* An input that will not allow the InputEventGrabber {@link InputProcessor}
* to be overwritten via setInputProcessor
*
* @author Lukas Böhm
*/
private class GrabberKeeper extends InputProxy {
@Override
public void setProxiedInput(Input proxied) {
super.setProxiedInput(proxied);
if (proxied.getInputProcessor() != grabber) {
grabber.setProxied(proxied.getInputProcessor());
proxied.setInputProcessor(grabber);
}
}
@Override
public void setInputProcessor(InputProcessor processor) {
grabber.setProxied(processor);
}
}
/**
* Pretends to be an InputProcessor to have one of its method called in
* processEvents, the place right after the input event buffers are filled
* and right before they are cleared.
*
* @author Lukas Böhm
*/
private class InputEventGrabber extends InputProcessorProxy {
private boolean armed = false;
public void rearm() {
armed = true;
}
@Override
protected synchronized void onEvent() {
if (armed) {
armed = false;
currentState.set(Gdx.input, onProcessEventsTrackFlags, false);
}
}
}
/**
* A runnable on the main thread to make the InputEventGrabber listen to
* events (arm it) just before the first event of the current main loop
* cycle is being processed
*
* @author Lukas Böhm
*/
private class GrabberArmer implements Runnable {
private boolean running;
public void start() {
running = true;
run();
}
public void stop() {
running = false;
}
@Override
public void run() {
grabber.rearm();
if (running) {
Gdx.app.postRunnable(this);
}
}
}
/**
* A worker thread to process and write input state changes back without
* blocking the main loop
*
* @author Lukas Böhm
*/
private class Processor implements Runnable {
private Thread processorThread;
private final InputStateProcessor processor;
public Processor() {
processor = new InputStateProcessor(recorder);
}
public void start() {
if (processorThread != null) {
if (processorThread.isAlive()) {
return;
}
synchronized (processorThread) {
processorThread = new Thread(this);
processorThread.setDaemon(true);
processorThread.start();
}
} else {
processorThread = new Thread(this);
processorThread.setDaemon(true);
processorThread.start();
}
}
public void stop() {
if (processorThread != null && processorThread.isAlive()) {
synchronized (processorThread) {
processorThread.interrupt();
try {
processorThread.join();
processor.reset();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* Processes all the InputStates that were collected for some main loop
* cycle in one run. That is, creating diffs and writing them using the
* {@link InputRecorder}'s {@link InputRecordWriter}
*/
private void process() {
List<InputState> swap = storedStates;
synchronized (storedStates) {
storedStates = processedStates;
}
processedStates = swap;
for (InputState state : processedStates) {
try {
processor.process(state);
} catch (IOException e) {
recorder.notifyError(e);
}
statePool.free(state);
}
processedStates.clear();
}
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
synchronized (this) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// need to finalize what is still in the queue
process();
}
}
}
/**
* Runnable to be posted on the main loop's thread to repeatedly call
* {@link InputStateTracker#track() track()}
*
* @author Lukas Böhm
*/
private class Tracker implements Runnable {
private boolean running = false;
public synchronized void start() {
boolean wasRunning = running;
running = true;
if (!wasRunning) {
Gdx.app.postRunnable(this);
}
}
public synchronized void stop() {
running = false;
}
@Override
public void run() {
track();
if (running) {
Gdx.app.postRunnable(this);
}
}
}
}