Initial replay recording support.

- Added listener and events in Game state to record replay frames.
- Send more accurate keys.

Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
This commit is contained in:
Jeffrey Han 2015-03-10 22:37:23 -04:00
parent 7536056a59
commit 790a66ec1e
6 changed files with 123 additions and 30 deletions

View File

@ -1238,6 +1238,9 @@ public class GameData {
if (replay != null)
return replay;
if (frames == null)
return null;
replay = new Replay();
replay.mode = OsuFile.MODE_OSU;
replay.version = Updater.get().getBuildDate();

View File

@ -40,7 +40,7 @@ public class OsuWriter {
/**
* Constructor.
* @param file the file to write to
* @throws FileNotFoundException
* @throws FileNotFoundException
*/
public OsuWriter(File file) throws FileNotFoundException {
this(new FileOutputStream(file));
@ -61,7 +61,7 @@ public class OsuWriter {
/**
* Closes the output stream.
* @throws IOException
* @throws IOException
*/
public void close() throws IOException { writer.close(); }

View File

@ -428,7 +428,7 @@ public class OsuDB {
* Sets all OsuFile non-array fields using a given result set.
* @param rs the result set containing the fields
* @param osu the OsuFile
* @throws SQLException
* @throws SQLException
*/
private static void setOsuFileFields(ResultSet rs, OsuFile osu) throws SQLException {
try {
@ -476,7 +476,7 @@ public class OsuDB {
* Sets all OsuFile array fields using a given result set.
* @param rs the result set containing the fields
* @param osu the OsuFile
* @throws SQLException
* @throws SQLException
*/
private static void setOsuFileArrayFields(ResultSet rs, OsuFile osu) throws SQLException {
try {

View File

@ -237,9 +237,9 @@ public class Replay {
// life data
StringBuilder sb = new StringBuilder();
if (lifeFrames != null) {
NumberFormat nf = new DecimalFormat("##.##");
NumberFormat nf = new DecimalFormat("##.##");
for (int i = 0; i < lifeFrames.length; i++) {
LifeFrame frame = lifeFrames[i];
LifeFrame frame = lifeFrames[i];
sb.append(String.format("%d|%s,",
frame.getTime(), nf.format(frame.getPercentage())));
}

View File

@ -44,8 +44,16 @@ public class ReplayFrame {
private float x, y;
/** Keys pressed (bitmask). */
private int keys;
private int keys;
/**
* Returns the start frame.
* @param t the value for the {@code time} and {@code timeDiff} fields
*/
public static ReplayFrame getStartFrame(int t) {
return new ReplayFrame(t, t, 256, -500, 0);
}
/**
* Constructor.
* @param timeDiff time since the previous action (in ms)
@ -72,6 +80,11 @@ public class ReplayFrame {
*/
public int getTimeDiff() { return timeDiff; }
/**
* Sets the time since the previous action, in milliseconds.
*/
public void setTimeDiff(int diff) { this.timeDiff = diff; }
/**
* Returns the scaled cursor x coordinate.
*/

View File

@ -173,11 +173,17 @@ public class Game extends BasicGameState {
/** Whether or not the replay thread should continue running. */
private boolean replayThreadRunning;
/** The last replay frame time. */
private int lastReplayTime = 0;
/** The last game keys pressed. */
private int lastKeysPressed = ReplayFrame.KEY_NONE;
/** The previous game mod state (before the replay). */
private int previousMods = 0;
/** The list of current replay frames (for recording replays). */
private LinkedList<ReplayFrame> frameList;
private LinkedList<ReplayFrame> replayFrames;
// game-related variables
private GameContainer container;
@ -468,14 +474,28 @@ public class Game extends BasicGameState {
if (MusicController.trackEnded() && objectIndex < hitObjects.length)
hitObjects[objectIndex].update(true, delta, mouseX, mouseY, false);
if (checkpointLoaded) // if checkpoint used, skip ranking screen
// if checkpoint used, skip ranking screen
if (checkpointLoaded)
game.closeRequested();
else { // go to ranking screen
// go to ranking screen
else {
((GameRanking) game.getState(Opsu.STATE_GAMERANKING)).setGameData(data);
ScoreData score = data.getScoreData(osu, (frameList == null) ? null :
frameList.toArray(new ReplayFrame[frameList.size()]));
ReplayFrame[] rf = null;
if (!isReplay && replayFrames != null) {
// finalize replay frames with start/skip frames
if (!replayFrames.isEmpty())
replayFrames.getFirst().setTimeDiff(replaySkipTime * -1);
replayFrames.addFirst(ReplayFrame.getStartFrame(replaySkipTime));
replayFrames.addFirst(ReplayFrame.getStartFrame(0));
rf = replayFrames.toArray(new ReplayFrame[replayFrames.size()]);
}
ScoreData score = data.getScoreData(osu, rf);
// add score to database
if (!GameMod.AUTO.isActive() && !GameMod.RELAX.isActive() && !GameMod.AUTOPILOT.isActive() && !isReplay)
ScoreDB.addScore(score);
game.enterState(Opsu.STATE_GAMERANKING, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
}
return;
@ -587,9 +607,9 @@ public class Game extends BasicGameState {
// game keys
if (!Keyboard.isRepeatEvent() && !isReplay) {
if (key == Options.getGameKeyLeft())
gameKeyPressed(Input.MOUSE_LEFT_BUTTON, input.getMouseX(), input.getMouseY());
gameKeyPressed(ReplayFrame.KEY_K1, input.getMouseX(), input.getMouseY());
else if (key == Options.getGameKeyRight())
gameKeyPressed(Input.MOUSE_RIGHT_BUTTON, input.getMouseX(), input.getMouseY());
gameKeyPressed(ReplayFrame.KEY_K2, input.getMouseX(), input.getMouseY());
}
switch (key) {
@ -715,16 +735,16 @@ public class Game extends BasicGameState {
return;
}
gameKeyPressed(button, x, y);
gameKeyPressed((button == Input.MOUSE_LEFT_BUTTON) ? ReplayFrame.KEY_M1 : ReplayFrame.KEY_M2, x, y);
}
/**
* Handles a game key pressed event.
* @param button the index of the button pressed
* @param keys the game keys pressed
* @param x the mouse x coordinate
* @param y the mouse y coordinate
*/
private void gameKeyPressed(int button, int x, int y) {
private void gameKeyPressed(int keys, int x, int y) {
// returning from pause screen
if (pauseTime > -1) {
double distance = Math.hypot(pausedMouseX - x, pausedMouseY - y);
@ -755,6 +775,8 @@ public class Game extends BasicGameState {
if (GameMod.AUTO.isActive() || GameMod.RELAX.isActive())
return;
addReplayFrame(x, y, lastKeysPressed | keys);
// circles
if (hitObject.isCircle() && hitObjects[objectIndex].mousePressed(x, y))
objectIndex++; // circle hit
@ -764,6 +786,36 @@ public class Game extends BasicGameState {
hitObjects[objectIndex].mousePressed(x, y);
}
@Override
public void mouseReleased(int button, int x, int y) {
if (Options.isMouseDisabled())
return;
if (button == Input.MOUSE_MIDDLE_BUTTON)
return;
int key = (button == Input.MOUSE_LEFT_BUTTON) ? ReplayFrame.KEY_M1 : ReplayFrame.KEY_M2;
if ((lastKeysPressed & key) > 0)
addReplayFrame(x, y, ReplayFrame.KEY_NONE);
}
@Override
public void keyReleased(int key, char c) {
if ((key == Options.getGameKeyLeft() && (lastKeysPressed & ReplayFrame.KEY_K1) > 0) ||
(key == Options.getGameKeyRight() && (lastKeysPressed & ReplayFrame.KEY_K2) > 0))
addReplayFrame(input.getMouseX(), input.getMouseY(), ReplayFrame.KEY_NONE);
}
@Override
public void mouseMoved(int oldx, int oldy, int newx, int newy) {
addReplayFrame(newx, newy, lastKeysPressed);
}
@Override
public void mouseDragged(int oldx, int oldy, int newx, int newy) {
addReplayFrame(newx, newy, lastKeysPressed);
}
@Override
public void mouseWheelMoved(int newValue) {
if (Options.isMouseWheelDisabled() || Options.isMouseDisabled())
@ -869,22 +921,16 @@ public class Game extends BasicGameState {
while (replayThreadRunning) {
// update frames
int trackPosition = MusicController.getPosition();
int keys = ReplayFrame.KEY_NONE;
while (replayIndex < replay.frames.length && trackPosition >= replay.frames[replayIndex].getTime()) {
ReplayFrame frame = replay.frames[replayIndex];
replayX = frame.getX();
replayY = frame.getY();
replayKeyPressed = frame.isKeyPressed();
if (replayKeyPressed)
keys = frame.getKeys();
if (replayKeyPressed) // send a key press
gameKeyPressed(frame.getKeys(), replayX, replayY);
replayIndex++;
}
// send a key press
if (replayKeyPressed && keys != ReplayFrame.KEY_NONE)
gameKeyPressed(((keys & ReplayFrame.KEY_M1) > 0) ?
Input.MOUSE_LEFT_BUTTON : Input.MOUSE_RIGHT_BUTTON, replayX, replayY);
// out of frames
if (replayIndex >= replay.frames.length)
break;
@ -898,8 +944,16 @@ public class Game extends BasicGameState {
};
replayThreadRunning = true;
replayThread.start();
} else
frameList = new LinkedList<ReplayFrame>();
}
// initialize replay-recording structures
else {
lastReplayTime = 0;
lastKeysPressed = ReplayFrame.KEY_NONE;
replaySkipTime = -1;
replayFrames = new LinkedList<ReplayFrame>();
replayFrames.add(new ReplayFrame(0, 0, input.getMouseX(), input.getMouseY(), 0));
}
leadInTime = osu.audioLeadIn + approachTime;
restart = Restart.FALSE;
@ -944,7 +998,7 @@ public class Game extends BasicGameState {
checkpointLoaded = false;
deaths = 0;
deathTime = -1;
frameList = null;
replayFrames = null;
System.gc();
}
@ -961,7 +1015,7 @@ public class Game extends BasicGameState {
leadInTime = 0;
MusicController.resume();
}
replaySkipTime = -1;
replaySkipTime = (isReplay) ? -1 : trackPosition;
if (replayThread != null && replayThread.isAlive())
replayThread.interrupt();
MusicController.setPosition(firstObjectTime - SKIP_OFFSET);
@ -1109,7 +1163,30 @@ public class Game extends BasicGameState {
* @param replay the replay
*/
public void setReplay(Replay replay) {
if (replay.frames == null) {
ErrorHandler.error("Invalid replay.", null, false);
return;
}
this.isReplay = true;
this.replay = replay;
}
/**
* Adds a replay frame to the list.
* @param x the cursor x coordinate
* @param y the cursor y coordinate
* @param keys the keys pressed
*/
private void addReplayFrame(int x, int y, int keys) {
if (isReplay)
return;
int time = MusicController.getPosition();
int timeDiff = time - lastReplayTime;
lastReplayTime = time;
lastKeysPressed = keys;
int cx = (int) ((x - OsuHitObject.getXOffset()) / OsuHitObject.getXMultiplier());
int cy = (int) ((y - OsuHitObject.getYOffset()) / OsuHitObject.getYMultiplier());
replayFrames.add(new ReplayFrame(timeDiff, time, cx, cy, lastKeysPressed));
}
}