Initial replay parsing support.

- Basic implementation of viewing replays in the Game state.
- Added OsuReader class for reading certain osu! file types. (author: Markus Jarderot)
- Added Replay, ReplayFrame, and LifeFrame classes to capture replay data. (author: smoogipooo)
- Added 'keyPressed' parameter to HitObject.update().
- Added cursor-drawing methods in UI that take the mouse coordinates and pressed state as parameters.
- Added GameMod methods to retrieve/load the active mods state as a bitmask.
- Added Apache commons-compress dependency for handling LZMA decompression.

Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
This commit is contained in:
Jeffrey Han
2015-03-09 18:32:43 -04:00
parent ef67387674
commit f6412f06e8
17 changed files with 761 additions and 36 deletions

View File

@@ -40,6 +40,8 @@ import itdelatrisu.opsu.objects.Circle;
import itdelatrisu.opsu.objects.HitObject;
import itdelatrisu.opsu.objects.Slider;
import itdelatrisu.opsu.objects.Spinner;
import itdelatrisu.opsu.replay.Replay;
import itdelatrisu.opsu.replay.ReplayFrame;
import java.io.File;
import java.util.Stack;
@@ -146,6 +148,24 @@ public class Game extends BasicGameState {
/** Number of retries. */
private int retries = 0;
/** The replay, if any. */
private Replay replay;
/** The current replay frame index. */
private int replayIndex = 0;
/** The replay cursor coordinates. */
private int replayX, replayY;
/** Whether a replay key is currently pressed. */
private boolean replayKeyPressed;
/** The replay skip time, or -1 if none. */
private int replaySkipTime = -1;
/** The previous game mod state (before the replay). */
private int previousMods = 0;
// game-related variables
private GameContainer container;
private StateBasedGame game;
@@ -365,14 +385,24 @@ public class Game extends BasicGameState {
cursorCirclePulse.drawCentered(pausedMouseX, pausedMouseY);
}
UI.draw(g);
if (replay == null)
UI.draw(g);
else
UI.draw(g, replayX, replayY, replayKeyPressed);
}
@Override
public void update(GameContainer container, StateBasedGame game, int delta)
throws SlickException {
UI.update(delta);
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
int mouseX, mouseY;
if (replay == null) {
mouseX = input.getMouseX();
mouseY = input.getMouseY();
} else {
mouseX = replayX;
mouseY = replayY;
}
skipButton.hoverUpdate(delta, mouseX, mouseY);
if (isLeadIn()) { // stop updating during song lead-in
@@ -423,14 +453,14 @@ public class Game extends BasicGameState {
if (objectIndex >= hitObjects.length || (MusicController.trackEnded() && objectIndex > 0)) {
// track ended before last object was processed: force a hit result
if (MusicController.trackEnded() && objectIndex < hitObjects.length)
hitObjects[objectIndex].update(true, delta, mouseX, mouseY);
hitObjects[objectIndex].update(true, delta, mouseX, mouseY, false);
if (checkpointLoaded) // if checkpoint used, skip ranking screen
game.closeRequested();
else { // go to ranking screen
((GameRanking) game.getState(Opsu.STATE_GAMERANKING)).setGameData(data);
ScoreData score = data.getScoreData(osu);
if (!GameMod.AUTO.isActive() && !GameMod.RELAX.isActive() && !GameMod.AUTOPILOT.isActive())
if (!GameMod.AUTO.isActive() && !GameMod.RELAX.isActive() && !GameMod.AUTOPILOT.isActive() && replay == null)
ScoreDB.addScore(score);
game.enterState(Opsu.STATE_GAMERANKING, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
}
@@ -453,6 +483,32 @@ public class Game extends BasicGameState {
}
}
// replays
if (replay != null) {
// skip intro
if (replaySkipTime > 0 && trackPosition > replaySkipTime) {
skipIntro();
replaySkipTime = -1;
}
// load next frame(s)
int replayKey = 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)
replayKey = frame.getKeys();
replayIndex++;
}
// send a key press
if (replayKey != ReplayFrame.KEY_NONE)
gameKeyPressed(((replayKey & ReplayFrame.KEY_M1) > 0) ?
Input.MOUSE_LEFT_BUTTON : Input.MOUSE_RIGHT_BUTTON, replayX, replayY);
}
// song beginning
if (objectIndex == 0 && trackPosition < osu.objects[0].getTime())
return; // nothing to do here
@@ -478,7 +534,7 @@ public class Game extends BasicGameState {
}
// pause game if focus lost
if (!container.hasFocus() && !GameMod.AUTO.isActive()) {
if (!container.hasFocus() && !GameMod.AUTO.isActive() && replay == null) {
if (pauseTime < 0) {
pausedMouseX = mouseX;
pausedMouseY = mouseY;
@@ -503,18 +559,21 @@ public class Game extends BasicGameState {
}
// game over, force a restart
restart = Restart.LOSE;
game.enterState(Opsu.STATE_GAMEPAUSEMENU);
if (replay == null) {
restart = Restart.LOSE;
game.enterState(Opsu.STATE_GAMEPAUSEMENU);
}
}
// update objects (loop in unlikely event of any skipped indexes)
boolean keyPressed = ((replay != null && replayKeyPressed) || Utils.isGameKeyPressed());
while (objectIndex < hitObjects.length && trackPosition > osu.objects[objectIndex].getTime()) {
// check if we've already passed the next object's start time
boolean overlap = (objectIndex + 1 < hitObjects.length &&
trackPosition > osu.objects[objectIndex + 1].getTime() - hitResultOffset[GameData.HIT_300]);
// update hit object and check completion status
if (hitObjects[objectIndex].update(overlap, delta, mouseX, mouseY))
if (hitObjects[objectIndex].update(overlap, delta, mouseX, mouseY, keyPressed))
objectIndex++; // done, so increment object index
else
break;
@@ -529,7 +588,7 @@ public class Game extends BasicGameState {
int trackPosition = MusicController.getPosition();
// game keys
if (!Keyboard.isRepeatEvent()) {
if (!Keyboard.isRepeatEvent() && replay == null) {
if (key == Options.getGameKeyLeft())
gameKeyPressed(Input.MOUSE_LEFT_BUTTON, input.getMouseX(), input.getMouseY());
else if (key == Options.getGameKeyRight())
@@ -538,8 +597,8 @@ public class Game extends BasicGameState {
switch (key) {
case Input.KEY_ESCAPE:
// "auto" mod: go back to song menu
if (GameMod.AUTO.isActive()) {
// "auto" mod or watching replay: go back to song menu
if (GameMod.AUTO.isActive() || replay != null) {
game.closeRequested();
break;
}
@@ -636,6 +695,10 @@ public class Game extends BasicGameState {
if (Options.isMouseDisabled())
return;
// watching replay
if (replay != null)
return;
// mouse wheel: pause the game
if (button == Input.MOUSE_MIDDLE_BUTTON && !Options.isMouseWheelDisabled()) {
int trackPosition = MusicController.getPosition();
@@ -769,6 +832,31 @@ public class Game extends BasicGameState {
}
}
// load replay frames
if (replay != null) {
// load mods
previousMods = GameMod.getModState();
GameMod.loadModState(replay.mods);
// load initial data
replayX = container.getWidth() / 2;
replayY = container.getHeight() / 2;
replayKeyPressed = false;
replaySkipTime = -1;
for (replayIndex = 0; replayIndex < replay.frames.length; replayIndex++) {
ReplayFrame frame = replay.frames[replayIndex];
if (frame.getY() < 0) { // skip time (?)
if (frame.getTime() > 0)
replaySkipTime = frame.getTime();
} else if (frame.getTime() == 0) {
replayX = frame.getX();
replayY = frame.getY();
replayKeyPressed = frame.isKeyPressed();
} else
break;
}
}
leadInTime = osu.audioLeadIn + approachTime;
restart = Restart.FALSE;
}
@@ -776,11 +864,15 @@ public class Game extends BasicGameState {
skipButton.resetHover();
}
// @Override
// public void leave(GameContainer container, StateBasedGame game)
// throws SlickException {
@Override
public void leave(GameContainer container, StateBasedGame game)
throws SlickException {
// container.setMouseGrabbed(false);
// }
// reset previous mod state
if (replay != null)
GameMod.loadModState(previousMods);
}
/**
* Resets all game data and structures.
@@ -816,12 +908,12 @@ public class Game extends BasicGameState {
private boolean skipIntro() {
int firstObjectTime = osu.objects[0].getTime();
int trackPosition = MusicController.getPosition();
if (objectIndex == 0 &&
trackPosition < firstObjectTime - SKIP_OFFSET) {
if (objectIndex == 0 && trackPosition < firstObjectTime - SKIP_OFFSET) {
if (isLeadIn()) {
leadInTime = 0;
MusicController.resume();
}
replaySkipTime = -1;
MusicController.setPosition(firstObjectTime - SKIP_OFFSET);
SoundController.playSound(SoundEffect.MENUHIT);
return true;
@@ -950,4 +1042,10 @@ public class Game extends BasicGameState {
* Returns the slider multiplier given by the current timing point.
*/
public float getTimingPointMultiplier() { return beatLength / beatLengthBase; }
/**
* Sets a replay to view.
* @param replay the replay
*/
public void setReplay(Replay replay) { this.replay = replay; }
}

View File

@@ -41,6 +41,7 @@ import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.db.OsuDB;
import itdelatrisu.opsu.db.ScoreDB;
import itdelatrisu.opsu.replay.Replay;
import itdelatrisu.opsu.states.ButtonMenu.MenuState;
import java.io.File;