Merge pull request #52 from fluddokt/ReplayTest

Replay Changes:
- Saves replay frames as if its repaying, then plays the replay at exact track position. This should give out exact results as the score screen.
- Calculates MD5 for beatmap files. (doesn't currently help anything)
- Reverse Slider ball (fixes #50)
This commit is contained in:
Jeffrey Han 2015-03-19 02:05:20 -04:00
commit ba20953634
8 changed files with 276 additions and 200 deletions

View File

@ -1241,7 +1241,7 @@ public class GameData {
* @param frames the replay frames * @param frames the replay frames
* @return the Replay object, or null if none exists and frames is null * @return the Replay object, or null if none exists and frames is null
*/ */
public Replay getReplay(ReplayFrame[] frames) { public Replay getReplay(ReplayFrame[] frames, OsuFile file) {
if (replay != null && frames == null) if (replay != null && frames == null)
return replay; return replay;
@ -1251,7 +1251,7 @@ public class GameData {
replay = new Replay(); replay = new Replay();
replay.mode = OsuFile.MODE_OSU; replay.mode = OsuFile.MODE_OSU;
replay.version = Updater.get().getBuildDate(); replay.version = Updater.get().getBuildDate();
replay.beatmapHash = ""; // TODO replay.beatmapHash = Utils.getMD5(file.getFile());
replay.playerName = ""; // TODO replay.playerName = ""; // TODO
replay.replayHash = Long.toString(System.currentTimeMillis()); // TODO replay.replayHash = Long.toString(System.currentTimeMillis()); // TODO
replay.hit300 = (short) hitResultCount[HIT_300]; replay.hit300 = (short) hitResultCount[HIT_300];

View File

@ -25,8 +25,10 @@ import itdelatrisu.opsu.downloads.DownloadNode;
import java.awt.Font; import java.awt.Font;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
@ -34,6 +36,8 @@ import java.net.HttpURLConnection;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.net.URL; import java.net.URL;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -537,4 +541,37 @@ public class Utils {
return s.useDelimiter("\\A").hasNext() ? s.next() : ""; return s.useDelimiter("\\A").hasNext() ? s.next() : "";
} }
} }
/**
* Returns the md5 hash of a file in hex form.
* @param file
* @return the md5 hash
*/
public static String getMD5(File file){
try {
InputStream in = new BufferedInputStream(new FileInputStream(file));
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] buf = new byte[4096];
while(true) {
int len = in.read(buf);
if (len < 0)
break;
md.update(buf, 0, len);
}
in.close();
byte[] md5byte = md.digest();
StringBuilder result = new StringBuilder();
for (byte b : md5byte) {
result.append(String.format("%02x", b));
}
return result.toString();
} catch (NoSuchAlgorithmException | IOException e) {
e.printStackTrace();
Log.error(e);
}
return null;
}
} }

View File

@ -132,11 +132,10 @@ public class Circle implements HitObject {
} }
@Override @Override
public boolean mousePressed(int x, int y) { public boolean mousePressed(int x, int y, int trackPosition) {
double distance = Math.hypot(this.x - x, this.y - y); double distance = Math.hypot(this.x - x, this.y - y);
int circleRadius = GameImage.HITCIRCLE.getImage().getWidth() / 2; int circleRadius = GameImage.HITCIRCLE.getImage().getWidth() / 2;
if (distance < circleRadius) { if (distance < circleRadius) {
int trackPosition = MusicController.getPosition();
int timeDiff = trackPosition - hitObject.getTime(); int timeDiff = trackPosition - hitObject.getTime();
int result = hitResult(timeDiff); int result = hitResult(timeDiff);
@ -150,10 +149,9 @@ public class Circle implements HitObject {
} }
@Override @Override
public boolean update(boolean overlap, int delta, int mouseX, int mouseY, boolean keyPressed) { public boolean update(boolean overlap, int delta, int mouseX, int mouseY, boolean keyPressed, int trackPosition) {
int time = hitObject.getTime(); int time = hitObject.getTime();
int trackPosition = MusicController.getPosition();
int[] hitResultOffset = game.getHitResultOffsets(); int[] hitResultOffset = game.getHitResultOffsets();
boolean isAutoMod = GameMod.AUTO.isActive(); boolean isAutoMod = GameMod.AUTO.isActive();
@ -176,7 +174,7 @@ public class Circle implements HitObject {
// "relax" mod: click automatically // "relax" mod: click automatically
else if (GameMod.RELAX.isActive() && trackPosition >= time) else if (GameMod.RELAX.isActive() && trackPosition >= time)
return mousePressed(mouseX, mouseY); return mousePressed(mouseX, mouseY, trackPosition);
return false; return false;
} }

View File

@ -38,17 +38,19 @@ public interface HitObject {
* @param mouseX the x coordinate of the mouse * @param mouseX the x coordinate of the mouse
* @param mouseY the y coordinate of the mouse * @param mouseY the y coordinate of the mouse
* @param keyPressed whether or not a game key is currently pressed * @param keyPressed whether or not a game key is currently pressed
* @param trackPosition the track Position
* @return true if object ended * @return true if object ended
*/ */
public boolean update(boolean overlap, int delta, int mouseX, int mouseY, boolean keyPressed); public boolean update(boolean overlap, int delta, int mouseX, int mouseY, boolean keyPressed, int trackPosition);
/** /**
* Processes a mouse click. * Processes a mouse click.
* @param x the x coordinate of the mouse * @param x the x coordinate of the mouse
* @param y the y coordinate of the mouse * @param y the y coordinate of the mouse
* @param trackPosition the track Position
* @return true if a hit result was processed * @return true if a hit result was processed
*/ */
public boolean mousePressed(int x, int y); public boolean mousePressed(int x, int y, int trackPosition);
/** /**
* Returns the coordinates of the hit object at a given track position. * Returns the coordinates of the hit object at a given track position.

View File

@ -24,13 +24,11 @@ import itdelatrisu.opsu.GameMod;
import itdelatrisu.opsu.OsuFile; import itdelatrisu.opsu.OsuFile;
import itdelatrisu.opsu.OsuHitObject; import itdelatrisu.opsu.OsuHitObject;
import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.objects.curves.CircumscribedCircle; import itdelatrisu.opsu.objects.curves.CircumscribedCircle;
import itdelatrisu.opsu.objects.curves.Curve; import itdelatrisu.opsu.objects.curves.Curve;
import itdelatrisu.opsu.objects.curves.LinearBezier; import itdelatrisu.opsu.objects.curves.LinearBezier;
import itdelatrisu.opsu.states.Game; import itdelatrisu.opsu.states.Game;
import org.newdawn.slick.Animation;
import org.newdawn.slick.Color; import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer; import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics; import org.newdawn.slick.Graphics;
@ -41,7 +39,7 @@ import org.newdawn.slick.Image;
*/ */
public class Slider implements HitObject { public class Slider implements HitObject {
/** Slider ball animation. */ /** Slider ball animation. */
private static Animation sliderBall; private static Image[] sliderBallImages;
/** Slider movement speed multiplier. */ /** Slider movement speed multiplier. */
private static float sliderMultiplier = 1.0f; private static float sliderMultiplier = 1.0f;
@ -111,7 +109,6 @@ public class Slider implements HitObject {
diameter = (int) (diameter * OsuHitObject.getXMultiplier()); // convert from Osupixels (640x480) diameter = (int) (diameter * OsuHitObject.getXMultiplier()); // convert from Osupixels (640x480)
// slider ball // slider ball
Image[] sliderBallImages;
if (GameImage.SLIDER_BALL.hasSkinImages() || if (GameImage.SLIDER_BALL.hasSkinImages() ||
(!GameImage.SLIDER_BALL.hasSkinImage() && GameImage.SLIDER_BALL.getImages() != null)) (!GameImage.SLIDER_BALL.hasSkinImage() && GameImage.SLIDER_BALL.getImages() != null))
sliderBallImages = GameImage.SLIDER_BALL.getImages(); sliderBallImages = GameImage.SLIDER_BALL.getImages();
@ -119,7 +116,6 @@ public class Slider implements HitObject {
sliderBallImages = new Image[]{ GameImage.SLIDER_BALL.getImage() }; sliderBallImages = new Image[]{ GameImage.SLIDER_BALL.getImage() };
for (int i = 0; i < sliderBallImages.length; i++) for (int i = 0; i < sliderBallImages.length; i++)
sliderBallImages[i] = sliderBallImages[i].getScaledCopy(diameter * 118 / 128, diameter * 118 / 128); sliderBallImages[i] = sliderBallImages[i].getScaledCopy(diameter * 118 / 128, diameter * 118 / 128);
sliderBall = new Animation(sliderBallImages, 30);
GameImage.SLIDER_FOLLOWCIRCLE.setImage(GameImage.SLIDER_FOLLOWCIRCLE.getImage().getScaledCopy(diameter * 259 / 128, diameter * 259 / 128)); GameImage.SLIDER_FOLLOWCIRCLE.setImage(GameImage.SLIDER_FOLLOWCIRCLE.getImage().getScaledCopy(diameter * 259 / 128, diameter * 259 / 128));
GameImage.REVERSEARROW.setImage(GameImage.REVERSEARROW.getImage().getScaledCopy(diameter, diameter)); GameImage.REVERSEARROW.setImage(GameImage.REVERSEARROW.getImage().getScaledCopy(diameter, diameter));
@ -202,7 +198,9 @@ public class Slider implements HitObject {
if (hitObject.getRepeatCount() - 1 > tcurRepeat) { if (hitObject.getRepeatCount() - 1 > tcurRepeat) {
Image arrow = GameImage.REVERSEARROW.getImage(); Image arrow = GameImage.REVERSEARROW.getImage();
if (tcurRepeat != currentRepeats) { if (tcurRepeat != currentRepeats) {
float t = getT(trackPosition, true); if (sliderTime == 0)
continue;
float t = Math.max(getT(trackPosition, true), 0);
arrow.setAlpha((float) (t - Math.floor(t))); arrow.setAlpha((float) (t - Math.floor(t)));
} else } else
arrow.setAlpha(1f); arrow.setAlpha(1f);
@ -218,22 +216,25 @@ public class Slider implements HitObject {
} }
} }
if (timeDiff >= 0) { if (timeDiff >= 0) {
// approach circle // approach circle
color.a = 1 - scale; color.a = 1 - scale;
GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale).drawCentered(x, y, color); GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale).drawCentered(x, y, color);
} else { } else {
//since update might not have run before drawing during replay,
//the slider time may not have been calculated.
//Which will cause NAN numbers and cause flicker.
if (sliderTime == 0)
return;
float[] c = curve.pointAt(getT(trackPosition, false)); float[] c = curve.pointAt(getT(trackPosition, false));
float[] c2 = curve.pointAt(getT(trackPosition, false) + 0.01f); float[] c2 = curve.pointAt(getT(trackPosition, false) + 0.01f);
// slider ball float t = getT(trackPosition, false);
// TODO: deprecated method //float dis = hitObject.getPixelLength()*OsuHitObject.getXMultiplier() * (t -(int)t);
// TODO 2: update the animation based on the distance traveled? //Image sliderBallFrame = sliderBallImages[ (int)(dis/ (diameter*Math.PI) *30)%sliderBallImages.length];
sliderBall.updateNoDraw(); Image sliderBallFrame = sliderBallImages[(int) (t * sliderTime * 60 / 1000) % sliderBallImages.length];
Image sliderBallFrame = sliderBall.getCurrentFrame();
float angle = (float) (Math.atan2(c2[1] - c[1], c2[0] - c[0]) * 180 / Math.PI); float angle = (float) (Math.atan2(c2[1] - c[1], c2[0] - c[0]) * 180 / Math.PI);
if (currentRepeats % 2 == 1)
angle += 180;
sliderBallFrame.setRotation(angle); sliderBallFrame.setRotation(angle);
sliderBallFrame.drawCentered(c[0], c[1]); sliderBallFrame.drawCentered(c[0], c[1]);
@ -283,14 +284,13 @@ public class Slider implements HitObject {
} }
@Override @Override
public boolean mousePressed(int x, int y) { public boolean mousePressed(int x, int y, int trackPosition) {
if (sliderClickedInitial) // first circle already processed if (sliderClickedInitial) // first circle already processed
return false; return false;
double distance = Math.hypot(this.x - x, this.y - y); double distance = Math.hypot(this.x - x, this.y - y);
int circleRadius = GameImage.HITCIRCLE.getImage().getWidth() / 2; int circleRadius = GameImage.HITCIRCLE.getImage().getWidth() / 2;
if (distance < circleRadius) { if (distance < circleRadius) {
int trackPosition = MusicController.getPosition();
int timeDiff = Math.abs(trackPosition - hitObject.getTime()); int timeDiff = Math.abs(trackPosition - hitObject.getTime());
int[] hitResultOffset = game.getHitResultOffsets(); int[] hitResultOffset = game.getHitResultOffsets();
@ -313,7 +313,7 @@ public class Slider implements HitObject {
} }
@Override @Override
public boolean update(boolean overlap, int delta, int mouseX, int mouseY, boolean keyPressed) { public boolean update(boolean overlap, int delta, int mouseX, int mouseY, boolean keyPressed, int trackPosition) {
int repeatCount = hitObject.getRepeatCount(); int repeatCount = hitObject.getRepeatCount();
// slider time and tick calculations // slider time and tick calculations
@ -334,7 +334,6 @@ public class Slider implements HitObject {
} }
} }
int trackPosition = MusicController.getPosition();
int[] hitResultOffset = game.getHitResultOffsets(); int[] hitResultOffset = game.getHitResultOffsets();
boolean isAutoMod = GameMod.AUTO.isActive(); boolean isAutoMod = GameMod.AUTO.isActive();
@ -362,7 +361,7 @@ public class Slider implements HitObject {
// "relax" mod: click automatically // "relax" mod: click automatically
else if (GameMod.RELAX.isActive() && trackPosition >= time) else if (GameMod.RELAX.isActive() && trackPosition >= time)
mousePressed(mouseX, mouseY); mousePressed(mouseX, mouseY, trackPosition);
} }
// end of slider // end of slider

View File

@ -197,11 +197,10 @@ public class Spinner implements HitObject {
} }
@Override @Override
public boolean mousePressed(int x, int y) { return false; } // not used public boolean mousePressed(int x, int y, int trackPosition) { return false; } // not used
@Override @Override
public boolean update(boolean overlap, int delta, int mouseX, int mouseY, boolean keyPressed) { public boolean update(boolean overlap, int delta, int mouseX, int mouseY, boolean keyPressed, int trackPosition) {
int trackPosition = MusicController.getPosition();
// end of spinner // end of spinner
if (overlap || trackPosition > hitObject.getEndTime()) { if (overlap || trackPosition > hitObject.getEndTime()) {

View File

@ -165,24 +165,18 @@ public class Game extends BasicGameState {
/** The replay cursor coordinates. */ /** The replay cursor coordinates. */
private int replayX, replayY; private int replayX, replayY;
/** The replay keys pressed. */
private int replayKeys;
/** Whether a replay key is currently pressed. */ /** Whether a replay key is currently pressed. */
private boolean replayKeyPressed; private boolean replayKeyPressed;
/** The replay skip time, or -1 if none. */ /** The replay skip time, or -1 if none. */
private int replaySkipTime = -1; private int replaySkipTime = -1;
/** The thread updating the replay frames. */
private Thread replayThread;
/** Whether or not the replay thread should continue running. */
private boolean replayThreadRunning;
/** The last replay frame time. */ /** The last replay frame time. */
private int lastReplayTime = 0; private int lastReplayTime = 0;
/** The keys from the previous replayFrame */
private int lastReplayKeys = 0;
/** The last game keys pressed. */ /** The last game keys pressed. */
private int lastKeysPressed = ReplayFrame.KEY_NONE; private int lastKeysPressed = ReplayFrame.KEY_NONE;
@ -517,14 +511,15 @@ public class Game extends BasicGameState {
cursorCirclePulse.drawCentered(pausedMouseX, pausedMouseY); cursorCirclePulse.drawCentered(pausedMouseX, pausedMouseY);
} }
if (GameMod.AUTO.isActive()) if (isReplay)
UI.draw(g, replayX, replayY, replayKeyPressed);
else if (GameMod.AUTO.isActive())
UI.draw(g, autoMouseX, autoMouseY, autoMousePressed); UI.draw(g, autoMouseX, autoMouseY, autoMousePressed);
else if (GameMod.AUTOPILOT.isActive()) else if (GameMod.AUTOPILOT.isActive())
UI.draw(g, autoMouseX, autoMouseY, Utils.isGameKeyPressed()); UI.draw(g, autoMouseX, autoMouseY, Utils.isGameKeyPressed());
else if (!isReplay)
UI.draw(g);
else else
UI.draw(g, replayX, replayY, replayKeyPressed); UI.draw(g);
} }
@Override @Override
@ -532,18 +527,41 @@ public class Game extends BasicGameState {
throws SlickException { throws SlickException {
UI.update(delta); UI.update(delta);
int mouseX, mouseY; int mouseX, mouseY;
if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) {
mouseX = autoMouseX;
mouseY = autoMouseY;
} else if (!isReplay) {
mouseX = input.getMouseX(); mouseX = input.getMouseX();
mouseY = input.getMouseY(); mouseY = input.getMouseY();
if (isLeadIn()) { // stop updating during song lead-in
leadInTime -= delta;
if (!isLeadIn())
MusicController.resume();
return;
}
//hover updates only the real mouse position
//but doesn't hover the replays
skipButton.hoverUpdate(delta, mouseX, mouseY);
int trackPosition = MusicController.getPosition();
if (!isReplay) {
addReplayFrameAndRun(mouseX, mouseY, lastKeysPressed, trackPosition);
} else { } else {
//out of frames, use previous data
if (replayIndex >= replay.frames.length){
updateGame(replayX, replayY, delta, MusicController.getPosition(), lastKeysPressed);
}
while (replayIndex < replay.frames.length && trackPosition >= replay.frames[replayIndex].getTime()) {
ReplayFrame frame = replay.frames[replayIndex];
replayX = frame.getScaledX();
replayY = frame.getScaledY();
replayKeyPressed = frame.isKeyPressed();
lastKeysPressed = frame.getKeys();
runReplayFrame(frame);
replayIndex++;
}
mouseX = replayX; mouseX = replayX;
mouseY = replayY; mouseY = replayY;
} }
skipButton.hoverUpdate(delta, mouseX, mouseY);
int trackPosition = MusicController.getPosition();
// "flashlight" mod: calculate visible area radius // "flashlight" mod: calculate visible area radius
if (GameMod.FLASHLIGHT.isActive()) { if (GameMod.FLASHLIGHT.isActive()) {
@ -600,12 +618,6 @@ public class Game extends BasicGameState {
} }
} }
if (isLeadIn()) { // stop updating during song lead-in
leadInTime -= delta;
if (!isLeadIn())
MusicController.resume();
return;
}
// returning from pause screen: must click previous mouse position // returning from pause screen: must click previous mouse position
if (pauseTime > -1) { if (pauseTime > -1) {
@ -632,6 +644,42 @@ public class Game extends BasicGameState {
return; return;
} }
data.updateDisplays(delta);
// replays
if (isReplay) {
// skip intro
if (replaySkipTime > 0 && trackPosition > replaySkipTime) {
skipIntro();
replaySkipTime = -1;
}
}
// pause game if focus lost
if (!container.hasFocus() && !GameMod.AUTO.isActive() && !isReplay) {
if (pauseTime < 0) {
pausedMouseX = mouseX;
pausedMouseY = mouseY;
pausePulse = 0f;
}
if (MusicController.isPlaying() || isLeadIn())
pauseTime = trackPosition;
game.enterState(Opsu.STATE_GAMEPAUSEMENU);
}
}
/**
* Updates the game
* @param mouseX the mouse x position
* @param mouseY
* @param delta
* @param trackPosition the track position
* @param keysPressed the keys that are pressed
*/
private void updateGame(int mouseX, int mouseY, int delta, int trackPosition, int keysPressed) {
// "Easy" mod: multiple "lives" // "Easy" mod: multiple "lives"
if (GameMod.EASY.isActive() && deathTime > -1) { if (GameMod.EASY.isActive() && deathTime > -1) {
if (data.getHealth() < 99f) if (data.getHealth() < 99f)
@ -642,13 +690,12 @@ public class Game extends BasicGameState {
} }
} }
data.updateDisplays(delta);
// map complete! // map complete!
if (objectIndex >= hitObjects.length || (MusicController.trackEnded() && objectIndex > 0)) { if (objectIndex >= hitObjects.length || (MusicController.trackEnded() && objectIndex > 0)) {
// track ended before last object was processed: force a hit result // track ended before last object was processed: force a hit result
if (MusicController.trackEnded() && objectIndex < hitObjects.length) if (MusicController.trackEnded() && objectIndex < hitObjects.length)
hitObjects[objectIndex].update(true, delta, mouseX, mouseY, false); hitObjects[objectIndex].update(true, delta, mouseX, mouseY, false, trackPosition);
// if checkpoint used, skip ranking screen // if checkpoint used, skip ranking screen
if (checkpointLoaded) if (checkpointLoaded)
@ -666,7 +713,7 @@ public class Game extends BasicGameState {
replayFrames.getFirst().setTimeDiff(replaySkipTime * -1); replayFrames.getFirst().setTimeDiff(replaySkipTime * -1);
replayFrames.addFirst(ReplayFrame.getStartFrame(replaySkipTime)); replayFrames.addFirst(ReplayFrame.getStartFrame(replaySkipTime));
replayFrames.addFirst(ReplayFrame.getStartFrame(0)); replayFrames.addFirst(ReplayFrame.getStartFrame(0));
Replay r = data.getReplay(replayFrames.toArray(new ReplayFrame[replayFrames.size()])); Replay r = data.getReplay(replayFrames.toArray(new ReplayFrame[replayFrames.size()]), osu);
if (r != null && !unranked) if (r != null && !unranked)
r.save(); r.save();
} }
@ -695,15 +742,6 @@ public class Game extends BasicGameState {
} }
} }
// replays
if (isReplay) {
// skip intro
if (replaySkipTime > 0 && trackPosition > replaySkipTime) {
skipIntro();
replaySkipTime = -1;
}
}
// song beginning // song beginning
if (objectIndex == 0 && trackPosition < osu.objects[0].getTime()) if (objectIndex == 0 && trackPosition < osu.objects[0].getTime())
return; // nothing to do here return; // nothing to do here
@ -728,18 +766,6 @@ public class Game extends BasicGameState {
} }
} }
// pause game if focus lost
if (!container.hasFocus() && !GameMod.AUTO.isActive() && !isReplay) {
if (pauseTime < 0) {
pausedMouseX = mouseX;
pausedMouseY = mouseY;
pausePulse = 0f;
}
if (MusicController.isPlaying() || isLeadIn())
pauseTime = trackPosition;
game.enterState(Opsu.STATE_GAMEPAUSEMENU);
}
// drain health // drain health
data.changeHealth(delta * -1 * GameData.HP_DRAIN_MULTIPLIER); data.changeHealth(delta * -1 * GameData.HP_DRAIN_MULTIPLIER);
if (!data.isAlive()) { if (!data.isAlive()) {
@ -761,14 +787,14 @@ public class Game extends BasicGameState {
} }
// update objects (loop in unlikely event of any skipped indexes) // update objects (loop in unlikely event of any skipped indexes)
boolean keyPressed = (isReplay) ? replayKeyPressed : Utils.isGameKeyPressed(); boolean keyPressed = keysPressed != ReplayFrame.KEY_NONE;
while (objectIndex < hitObjects.length && trackPosition > osu.objects[objectIndex].getTime()) { while (objectIndex < hitObjects.length && trackPosition > osu.objects[objectIndex].getTime()) {
// check if we've already passed the next object's start time // check if we've already passed the next object's start time
boolean overlap = (objectIndex + 1 < hitObjects.length && boolean overlap = (objectIndex + 1 < hitObjects.length &&
trackPosition > osu.objects[objectIndex + 1].getTime() - hitResultOffset[GameData.HIT_300]); trackPosition > osu.objects[objectIndex + 1].getTime() - hitResultOffset[GameData.HIT_300]);
// update hit object and check completion status // update hit object and check completion status
if (hitObjects[objectIndex].update(overlap, delta, mouseX, mouseY, keyPressed)) if (hitObjects[objectIndex].update(overlap, delta, mouseX, mouseY, keyPressed, trackPosition))
objectIndex++; // done, so increment object index objectIndex++; // done, so increment object index
else else
break; break;
@ -783,11 +809,15 @@ public class Game extends BasicGameState {
int trackPosition = MusicController.getPosition(); int trackPosition = MusicController.getPosition();
// game keys // game keys
if (!Keyboard.isRepeatEvent() && !isReplay) { int mouseX = input.getMouseX();
int mouseY = input.getMouseY();
if (!Keyboard.isRepeatEvent()) {
int keys = 0;
if (key == Options.getGameKeyLeft()) if (key == Options.getGameKeyLeft())
gameKeyPressed(ReplayFrame.KEY_K1, input.getMouseX(), input.getMouseY()); keys = ReplayFrame.KEY_K1;
else if (key == Options.getGameKeyRight()) else if (key == Options.getGameKeyRight())
gameKeyPressed(ReplayFrame.KEY_K2, input.getMouseX(), input.getMouseY()); keys = ReplayFrame.KEY_K2;
gameKeyPressed(keys, mouseX, mouseY, trackPosition);
} }
switch (key) { switch (key) {
@ -800,8 +830,8 @@ public class Game extends BasicGameState {
// pause game // pause game
if (pauseTime < 0 && breakTime <= 0 && trackPosition >= osu.objects[0].getTime()) { if (pauseTime < 0 && breakTime <= 0 && trackPosition >= osu.objects[0].getTime()) {
pausedMouseX = input.getMouseX(); pausedMouseX = mouseX;
pausedMouseY = input.getMouseY(); pausedMouseY = mouseY;
pausePulse = 0f; pausePulse = 0f;
} }
if (MusicController.isPlaying() || isLeadIn()) if (MusicController.isPlaying() || isLeadIn())
@ -846,7 +876,6 @@ public class Game extends BasicGameState {
if (checkpoint == 0 || checkpoint > osu.endTime) if (checkpoint == 0 || checkpoint > osu.endTime)
break; // invalid checkpoint break; // invalid checkpoint
try { try {
killReplayThread();
restart = Restart.MANUAL; restart = Restart.MANUAL;
enter(container, game); enter(container, game);
checkpointLoaded = true; checkpointLoaded = true;
@ -863,6 +892,7 @@ public class Game extends BasicGameState {
osu.objects[objectIndex++].getTime() <= checkpoint) osu.objects[objectIndex++].getTime() <= checkpoint)
; ;
objectIndex--; objectIndex--;
lastReplayTime = osu.objects[objectIndex].getTime();
} catch (SlickException e) { } catch (SlickException e) {
ErrorHandler.error("Failed to load checkpoint.", e, false); ErrorHandler.error("Failed to load checkpoint.", e, false);
} }
@ -913,7 +943,15 @@ public class Game extends BasicGameState {
return; return;
} }
gameKeyPressed((button == Input.MOUSE_LEFT_BUTTON) ? ReplayFrame.KEY_M1 : ReplayFrame.KEY_M2, x, y);
int keys = 0;
if (button == Input.MOUSE_LEFT_BUTTON)
keys = ReplayFrame.KEY_M1;
else if (button == Input.MOUSE_RIGHT_BUTTON)
keys = ReplayFrame.KEY_M2;
gameKeyPressed(keys, x, y, MusicController.getPosition());
} }
/** /**
@ -921,8 +959,9 @@ public class Game extends BasicGameState {
* @param keys the game keys pressed * @param keys the game keys pressed
* @param x the mouse x coordinate * @param x the mouse x coordinate
* @param y the mouse y coordinate * @param y the mouse y coordinate
* @param trackPosition the track Position
*/ */
private void gameKeyPressed(int keys, int x, int y) { private void gameKeyPressed(int keys, int x, int y, int trackPosition) {
// returning from pause screen // returning from pause screen
if (pauseTime > -1) { if (pauseTime > -1) {
double distance = Math.hypot(pausedMouseX - x, pausedMouseY - y); double distance = Math.hypot(pausedMouseX - x, pausedMouseY - y);
@ -938,42 +977,18 @@ public class Game extends BasicGameState {
return; return;
} }
if (objectIndex >= hitObjects.length) // nothing left to do here
return;
OsuHitObject hitObject = osu.objects[objectIndex];
// skip beginning // skip beginning
if (skipButton.contains(x, y)) { if (skipButton.contains(x, y)) {
if (skipIntro()) if (skipIntro())
return; // successfully skipped return; // successfully skipped
} }
// "auto" and "relax" mods: ignore user actions if(!isReplay && keys > 0) {
if (GameMod.AUTO.isActive() || GameMod.RELAX.isActive()) lastKeysPressed |= keys; //sets keys bits
return; addReplayFrameAndRun(x, y, lastKeysPressed, trackPosition);
}
// "autopilot" mod: ignore actual cursor coordinates
int cx, cy;
if (GameMod.AUTOPILOT.isActive()) {
cx = autoMouseX;
cy = autoMouseY;
} else {
cx = x;
cy = y;
} }
if (!isReplay)
addReplayFrame(cx, cy, lastKeysPressed | keys);
// circles
if (hitObject.isCircle() && hitObjects[objectIndex].mousePressed(cx, cy))
objectIndex++; // circle hit
// sliders
else if (hitObject.isSlider())
hitObjects[objectIndex].mousePressed(cx, cy);
}
@Override @Override
public void mouseReleased(int button, int x, int y) { public void mouseReleased(int button, int x, int y) {
@ -983,26 +998,44 @@ public class Game extends BasicGameState {
if (button == Input.MOUSE_MIDDLE_BUTTON) if (button == Input.MOUSE_MIDDLE_BUTTON)
return; return;
if (!isReplay && lastKeysPressed != ReplayFrame.KEY_NONE) int keys = 0;
addReplayFrame(x, y, ReplayFrame.KEY_NONE); if (button == Input.MOUSE_LEFT_BUTTON)
keys = ReplayFrame.KEY_M1;
else if (button == Input.MOUSE_RIGHT_BUTTON)
keys = ReplayFrame.KEY_M2;
gameKeyReleased(keys, x, y, MusicController.getPosition());
} }
@Override @Override
public void keyReleased(int key, char c) { public void keyReleased(int key, char c) {
if (!isReplay && lastKeysPressed != ReplayFrame.KEY_NONE && (key == Options.getGameKeyLeft() || key == Options.getGameKeyRight())) int keys = 0;
addReplayFrame(input.getMouseX(), input.getMouseY(), ReplayFrame.KEY_NONE); if (key == Options.getGameKeyLeft())
keys = ReplayFrame.KEY_K1;
else if (key == Options.getGameKeyRight())
keys = ReplayFrame.KEY_K2;
gameKeyReleased(keys, input.getMouseX(), input.getMouseY(), MusicController.getPosition());
}
/**
* Handles a game key Released event.
* @param keys the game keys released
* @param x the mouse x coordinate
* @param y the mouse y coordinate
* @param trackPosition the track Position
*/
private void gameKeyReleased(int keys, int x, int y, int trackPosition) {
if (!isReplay && keys > 0) {
lastKeysPressed &= ~keys; //clears keys bits
addReplayFrameAndRun(x, y, lastKeysPressed, trackPosition);
}
} }
@Override @Override
public void mouseMoved(int oldx, int oldy, int newx, int newy) { public void mouseMoved(int oldx, int oldy, int newx, int newy) {
if (!isReplay)
addReplayFrame(newx, newy, lastKeysPressed);
} }
@Override @Override
public void mouseDragged(int oldx, int oldy, int newx, int newy) { public void mouseDragged(int oldx, int oldy, int newx, int newy) {
if (!isReplay)
addReplayFrame(newx, newy, lastKeysPressed);
} }
@Override @Override
@ -1078,6 +1111,7 @@ public class Game extends BasicGameState {
if (GameMod.AUTO.isActive() || isReplay) if (GameMod.AUTO.isActive() || isReplay)
UI.showCursor(); UI.showCursor();
lastReplayTime = 0;
// load replay frames // load replay frames
if (isReplay) { if (isReplay) {
// load mods // load mods
@ -1087,7 +1121,6 @@ public class Game extends BasicGameState {
// load initial data // load initial data
replayX = container.getWidth() / 2; replayX = container.getWidth() / 2;
replayY = container.getHeight() / 2; replayY = container.getHeight() / 2;
replayKeys = ReplayFrame.KEY_NONE;
replayKeyPressed = false; replayKeyPressed = false;
replaySkipTime = -1; replaySkipTime = -1;
for (replayIndex = 0; replayIndex < replay.frames.length; replayIndex++) { for (replayIndex = 0; replayIndex < replay.frames.length; replayIndex++) {
@ -1098,56 +1131,14 @@ public class Game extends BasicGameState {
} else if (frame.getTime() == 0) { } else if (frame.getTime() == 0) {
replayX = frame.getScaledX(); replayX = frame.getScaledX();
replayY = frame.getScaledY(); replayY = frame.getScaledY();
replayKeys = frame.getKeys();
replayKeyPressed = frame.isKeyPressed(); replayKeyPressed = frame.isKeyPressed();
} else } else
break; break;
} }
// run frame updates in another thread
killReplayThread();
replayThread = new Thread() {
@Override
public void run() {
while (replayThreadRunning) {
// update frames
int trackPosition = MusicController.getPosition();
while (replayIndex < replay.frames.length && trackPosition >= replay.frames[replayIndex].getTime()) {
ReplayFrame frame = replay.frames[replayIndex];
replayX = frame.getScaledX();
replayY = frame.getScaledY();
replayKeyPressed = frame.isKeyPressed();
int keys = frame.getKeys();
if (replayKeyPressed && keys != replayKeys) // send a key press
gameKeyPressed(frame.getKeys(), replayX, replayY);
replayKeys = keys;
replayIndex++;
}
// out of frames
if (replayIndex >= replay.frames.length)
break;
// sleep execution
try {
int diff = replay.frames[replayIndex].getTime() - trackPosition - 1;
if (diff < 1)
Thread.sleep(0, 256000);
else
Thread.sleep(diff);
} catch (InterruptedException e) {}
}
}
};
if (!GameMod.AUTO.isActive()) { // "auto" mod: ignore replay frames
replayThreadRunning = true;
replayThread.start();
}
} }
// initialize replay-recording structures // initialize replay-recording structures
else { else {
lastReplayTime = 0;
lastKeysPressed = ReplayFrame.KEY_NONE; lastKeysPressed = ReplayFrame.KEY_NONE;
replaySkipTime = -1; replaySkipTime = -1;
replayFrames = new LinkedList<ReplayFrame>(); replayFrames = new LinkedList<ReplayFrame>();
@ -1173,7 +1164,6 @@ public class Game extends BasicGameState {
// replays // replays
if (isReplay) { if (isReplay) {
GameMod.loadModState(previousMods); GameMod.loadModState(previousMods);
killReplayThread();
} }
} }
@ -1318,10 +1308,9 @@ public class Game extends BasicGameState {
} }
MusicController.setPosition(firstObjectTime - SKIP_OFFSET); MusicController.setPosition(firstObjectTime - SKIP_OFFSET);
replaySkipTime = (isReplay) ? -1 : trackPosition; replaySkipTime = (isReplay) ? -1 : trackPosition;
if (replayThread != null && replayThread.isAlive()) { if (isReplay) {
replayX = (int) skipButton.getX(); replayX = (int) skipButton.getX();
replayY = (int) skipButton.getY(); replayY = (int) skipButton.getY();
replayThread.interrupt();
} }
SoundController.playSound(SoundEffect.MENUHIT); SoundController.playSound(SoundEffect.MENUHIT);
return true; return true;
@ -1451,16 +1440,6 @@ public class Game extends BasicGameState {
*/ */
public float getTimingPointMultiplier() { return beatLength / beatLengthBase; } public float getTimingPointMultiplier() { return beatLength / beatLengthBase; }
/**
* Kills the running replay updating thread, if any.
*/
private void killReplayThread() {
if (replayThread != null && replayThread.isAlive()) {
replayThreadRunning = false;
replayThread.interrupt();
}
replayThread = null;
}
/** /**
* Sets a replay to view, or resets the replay if null. * Sets a replay to view, or resets the replay if null.
@ -1481,19 +1460,81 @@ public class Game extends BasicGameState {
} }
/** /**
* Adds a replay frame to the list. * Adds a replay frame to the list if able and runs it.
* @param x the cursor x coordinate * @param x the cursor x coordinate
* @param y the cursor y coordinate * @param y the cursor y coordinate
* @param keys the keys pressed * @param keys the keys pressed
* @param time the time of the replay Frame
*/ */
private void addReplayFrame(int x, int y, int keys) { public synchronized void addReplayFrameAndRun(int x, int y, int keys, int time){
int time = MusicController.getPosition(); if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) {
x = autoMouseX;
y = autoMouseY;
}
ReplayFrame frame = addReplayFrame(x, y, keys, time);
if(frame != null)
runReplayFrame(frame);
}
/**
* Runs a replay Frame
* @param frame the frame to run
*/
private void runReplayFrame(ReplayFrame frame){
int keys = frame.getKeys();
int replayX = frame.getScaledX();
int replayY = frame.getScaledY();
int deltaKeys = (keys & ~lastReplayKeys ); //keys that turned on
if (deltaKeys > 0) { // send a key press
updateGameKeyPress(deltaKeys, replayX, replayY, frame.getTime());
} else if(keys != lastReplayKeys){
} else {
updateGame(replayX, replayY, frame.getTimeDiff(), frame.getTime(), keys);
}
lastReplayKeys = keys;
}
/**
* Updates the games mouse pressed
* @param mouseX the mouse x position
* @param mouseY
* @param trackPosition the track position
* @param keysPressed the keys that are pressed
*/
private void updateGameKeyPress(int keys, int x, int y, int trackPosition) {
if (objectIndex >= hitObjects.length) // nothing left to do here
return;
OsuHitObject hitObject = osu.objects[objectIndex];
// circles
if (hitObject.isCircle() && hitObjects[objectIndex].mousePressed(x, y, trackPosition))
objectIndex++; // circle hit
// sliders
else if (hitObject.isSlider())
hitObjects[objectIndex].mousePressed(x, y, trackPosition);
}
/**
* Adds a replay frame to the list if able.
* @param x the cursor x coordinate
* @param y the cursor y coordinate
* @param keys the keys pressed
* @param time the time of the replay Frame
* @return a Replay Frame representing the data
*/
private ReplayFrame addReplayFrame(int x, int y, int keys, int time) {
int timeDiff = time - lastReplayTime; int timeDiff = time - lastReplayTime;
lastReplayTime = time; lastReplayTime = time;
lastKeysPressed = keys;
int cx = (int) ((x - OsuHitObject.getXOffset()) / OsuHitObject.getXMultiplier()); int cx = (int) ((x - OsuHitObject.getXOffset()) / OsuHitObject.getXMultiplier());
int cy = (int) ((y - OsuHitObject.getYOffset()) / OsuHitObject.getYMultiplier()); int cy = (int) ((y - OsuHitObject.getYOffset()) / OsuHitObject.getYMultiplier());
replayFrames.add(new ReplayFrame(timeDiff, time, cx, cy, lastKeysPressed)); ReplayFrame tFrame = new ReplayFrame(timeDiff, time, cx, cy, keys);
if(replayFrames != null)
replayFrames.add(tFrame);
return tFrame;
} }
/** /**

View File

@ -170,7 +170,7 @@ public class GameRanking extends BasicGameState {
Game gameState = (Game) game.getState(Opsu.STATE_GAME); Game gameState = (Game) game.getState(Opsu.STATE_GAME);
boolean returnToGame = false; boolean returnToGame = false;
if (replayButton.contains(x, y)) { if (replayButton.contains(x, y)) {
Replay r = data.getReplay(null); Replay r = data.getReplay(null, null);
if (r != null) { if (r != null) {
try { try {
r.load(); r.load();