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:
parent
ef67387674
commit
f6412f06e8
5
pom.xml
5
pom.xml
|
@ -197,5 +197,10 @@
|
||||||
<artifactId>maven-artifact</artifactId>
|
<artifactId>maven-artifact</artifactId>
|
||||||
<version>3.0.3</version>
|
<version>3.0.3</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-compress</artifactId>
|
||||||
|
<version>1.8</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|
|
@ -1212,12 +1212,7 @@ public class GameData {
|
||||||
scoreData.score = score;
|
scoreData.score = score;
|
||||||
scoreData.combo = comboMax;
|
scoreData.combo = comboMax;
|
||||||
scoreData.perfect = (comboMax == fullObjectCount);
|
scoreData.perfect = (comboMax == fullObjectCount);
|
||||||
int mods = 0;
|
scoreData.mods = GameMod.getModState();
|
||||||
for (GameMod mod : GameMod.values()) {
|
|
||||||
if (mod.isActive())
|
|
||||||
mods |= mod.getBit();
|
|
||||||
}
|
|
||||||
scoreData.mods = mods;
|
|
||||||
return scoreData;
|
return scoreData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -213,6 +213,27 @@ public enum GameMod {
|
||||||
return scoreMultiplier;
|
return scoreMultiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current game mod state (bitwise OR of active mods).
|
||||||
|
*/
|
||||||
|
public static int getModState() {
|
||||||
|
int state = 0;
|
||||||
|
for (GameMod mod : GameMod.values()) {
|
||||||
|
if (mod.isActive())
|
||||||
|
state |= mod.getBit();
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the active states of all game mods to the given state.
|
||||||
|
* @param state the state (bitwise OR of active mods)
|
||||||
|
*/
|
||||||
|
public static void loadModState(int state) {
|
||||||
|
for (GameMod mod : GameMod.values())
|
||||||
|
mod.active = ((state & mod.getBit()) > 0);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
* @param category the category for the mod
|
* @param category the category for the mod
|
||||||
|
|
|
@ -129,6 +129,16 @@ public class OsuHitObject {
|
||||||
*/
|
*/
|
||||||
public static float getYMultiplier() { return yMultiplier; }
|
public static float getYMultiplier() { return yMultiplier; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the X offset for coordinates.
|
||||||
|
*/
|
||||||
|
public static int getXOffset() { return xOffset; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Y offset for coordinates.
|
||||||
|
*/
|
||||||
|
public static int getYOffset() { return yOffset; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
* @param line the line to be parsed
|
* @param line the line to be parsed
|
||||||
|
|
175
src/itdelatrisu/opsu/OsuReader.java
Normal file
175
src/itdelatrisu/opsu/OsuReader.java
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
/*
|
||||||
|
* opsu! - an open-source osu! client
|
||||||
|
* Copyright (C) 2014, 2015 Jeffrey Han
|
||||||
|
*
|
||||||
|
* opsu! is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* opsu! is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package itdelatrisu.opsu;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reader for osu! file types.
|
||||||
|
*
|
||||||
|
* @author Markus Jarderot (http://stackoverflow.com/questions/28788616)
|
||||||
|
*/
|
||||||
|
public class OsuReader {
|
||||||
|
/** Input stream reader. */
|
||||||
|
private DataInputStream reader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
* @param file the file to read from
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public OsuReader(File file) throws IOException {
|
||||||
|
this(new FileInputStream(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
* @param source the input stream to read from
|
||||||
|
*/
|
||||||
|
public OsuReader(InputStream source) {
|
||||||
|
this.reader = new DataInputStream(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the input stream in use.
|
||||||
|
*/
|
||||||
|
public InputStream getInputStream() { return reader; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the input stream.
|
||||||
|
*/
|
||||||
|
public void close() throws IOException { reader.close(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a 1-byte value.
|
||||||
|
*/
|
||||||
|
public byte readByte() throws IOException {
|
||||||
|
return this.reader.readByte();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a 2-byte little endian value.
|
||||||
|
*/
|
||||||
|
public short readShort() throws IOException {
|
||||||
|
byte[] bytes = new byte[2];
|
||||||
|
this.reader.readFully(bytes);
|
||||||
|
ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
return bb.getShort();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a 4-byte little endian value.
|
||||||
|
*/
|
||||||
|
public int readInt() throws IOException {
|
||||||
|
byte[] bytes = new byte[4];
|
||||||
|
this.reader.readFully(bytes);
|
||||||
|
ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
return bb.getInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads an 8-byte little endian value.
|
||||||
|
*/
|
||||||
|
public long readLong() throws IOException {
|
||||||
|
byte[] bytes = new byte[8];
|
||||||
|
this.reader.readFully(bytes);
|
||||||
|
ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
return bb.getLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a 4-byte little endian float.
|
||||||
|
*/
|
||||||
|
public float readSingle() throws IOException {
|
||||||
|
byte[] bytes = new byte[4];
|
||||||
|
this.reader.readFully(bytes);
|
||||||
|
ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
return bb.getFloat();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads an 8-byte little endian double.
|
||||||
|
*/
|
||||||
|
public double readDouble() throws IOException {
|
||||||
|
byte[] bytes = new byte[8];
|
||||||
|
this.reader.readFully(bytes);
|
||||||
|
ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
return bb.getDouble();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a 1-byte value as a boolean.
|
||||||
|
*/
|
||||||
|
public boolean readBoolean() throws IOException {
|
||||||
|
return this.reader.readBoolean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads an unsigned variable length integer (ULEB128).
|
||||||
|
*/
|
||||||
|
public int readULEB128() throws IOException {
|
||||||
|
int value = 0;
|
||||||
|
for (int shift = 0; shift < 32; shift += 7) {
|
||||||
|
byte b = this.reader.readByte();
|
||||||
|
value |= (b & 0x7F) << shift;
|
||||||
|
if (b >= 0)
|
||||||
|
return value; // MSB is zero. End of value.
|
||||||
|
}
|
||||||
|
throw new IOException("ULEB128 too large");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a variable-length string of 1-byte characters.
|
||||||
|
*/
|
||||||
|
public String readString() throws IOException {
|
||||||
|
// 00 = empty string
|
||||||
|
// 0B <length> <char>* = normal string
|
||||||
|
// <length> is encoded as an LEB, and is the byte length of the rest.
|
||||||
|
// <char>* is encoded as UTF8, and is the string content.
|
||||||
|
byte kind = this.reader.readByte();
|
||||||
|
if (kind == 0)
|
||||||
|
return "";
|
||||||
|
if (kind != 0x0B)
|
||||||
|
throw new IOException(String.format("String format error: Expected 0x0B or 0x00, found 0x%02X", kind & 0xFF));
|
||||||
|
int length = readULEB128();
|
||||||
|
if (length == 0)
|
||||||
|
return "";
|
||||||
|
byte[] utf8bytes = new byte[length];
|
||||||
|
this.reader.readFully(utf8bytes);
|
||||||
|
return new String(utf8bytes, "UTF-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads an 8-byte date in Windows ticks.
|
||||||
|
*/
|
||||||
|
public Date readDate() throws IOException {
|
||||||
|
long ticks = readLong();
|
||||||
|
final long TICKS_AT_EPOCH = 621355968000000000L;
|
||||||
|
final long TICKS_PER_MILLISECOND = 10000;
|
||||||
|
|
||||||
|
return new Date((ticks - TICKS_AT_EPOCH) / TICKS_PER_MILLISECOND);
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,7 @@
|
||||||
package itdelatrisu.opsu;
|
package itdelatrisu.opsu;
|
||||||
|
|
||||||
import itdelatrisu.opsu.GameData.Grade;
|
import itdelatrisu.opsu.GameData.Grade;
|
||||||
|
import itdelatrisu.opsu.replay.Replay;
|
||||||
import itdelatrisu.opsu.states.SongMenu;
|
import itdelatrisu.opsu.states.SongMenu;
|
||||||
|
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
|
@ -59,6 +60,9 @@ public class ScoreData implements Comparable<ScoreData> {
|
||||||
/** Game mod bitmask. */
|
/** Game mod bitmask. */
|
||||||
public int mods;
|
public int mods;
|
||||||
|
|
||||||
|
/** The replay. */
|
||||||
|
public Replay replay;
|
||||||
|
|
||||||
/** Time since the score was achieved. */
|
/** Time since the score was achieved. */
|
||||||
private String timeSince;
|
private String timeSince;
|
||||||
|
|
||||||
|
@ -153,6 +157,7 @@ public class ScoreData implements Comparable<ScoreData> {
|
||||||
this.combo = rs.getInt(15);
|
this.combo = rs.getInt(15);
|
||||||
this.perfect = rs.getBoolean(16);
|
this.perfect = rs.getBoolean(16);
|
||||||
this.mods = rs.getInt(17);
|
this.mods = rs.getInt(17);
|
||||||
|
// this.replay = ; // TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -120,6 +120,20 @@ public class UI {
|
||||||
drawCursor();
|
drawCursor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the global UI components: cursor, FPS, volume bar, bar notifications.
|
||||||
|
* @param g the graphics context
|
||||||
|
* @param mouseX the mouse x coordinate
|
||||||
|
* @param mouseY the mouse y coordinate
|
||||||
|
* @param mousePressed whether or not the mouse button is pressed
|
||||||
|
*/
|
||||||
|
public static void draw(Graphics g, int mouseX, int mouseY, boolean mousePressed) {
|
||||||
|
drawBarNotification(g);
|
||||||
|
drawVolume(g);
|
||||||
|
drawFPS();
|
||||||
|
drawCursor(mouseX, mouseY, mousePressed);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets the necessary UI components upon entering a state.
|
* Resets the necessary UI components upon entering a state.
|
||||||
*/
|
*/
|
||||||
|
@ -161,6 +175,21 @@ public class UI {
|
||||||
* Draws the cursor.
|
* Draws the cursor.
|
||||||
*/
|
*/
|
||||||
public static void drawCursor() {
|
public static void drawCursor() {
|
||||||
|
int state = game.getCurrentStateID();
|
||||||
|
boolean mousePressed =
|
||||||
|
(((state == Opsu.STATE_GAME || state == Opsu.STATE_GAMEPAUSEMENU) && Utils.isGameKeyPressed()) ||
|
||||||
|
((input.isMouseButtonDown(Input.MOUSE_LEFT_BUTTON) || input.isMouseButtonDown(Input.MOUSE_RIGHT_BUTTON)) &&
|
||||||
|
!(state == Opsu.STATE_GAME && Options.isMouseDisabled())));
|
||||||
|
drawCursor(input.getMouseX(), input.getMouseY(), mousePressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the cursor.
|
||||||
|
* @param mouseX the mouse x coordinate
|
||||||
|
* @param mouseY the mouse y coordinate
|
||||||
|
* @param mousePressed whether or not the mouse button is pressed
|
||||||
|
*/
|
||||||
|
public static void drawCursor(int mouseX, int mouseY, boolean mousePressed) {
|
||||||
// determine correct cursor image
|
// determine correct cursor image
|
||||||
// TODO: most beatmaps don't skin CURSOR_MIDDLE, so how to determine style?
|
// TODO: most beatmaps don't skin CURSOR_MIDDLE, so how to determine style?
|
||||||
Image cursor = null, cursorMiddle = null, cursorTrail = null;
|
Image cursor = null, cursorMiddle = null, cursorTrail = null;
|
||||||
|
@ -176,7 +205,6 @@ public class UI {
|
||||||
if (newStyle)
|
if (newStyle)
|
||||||
cursorMiddle = GameImage.CURSOR_MIDDLE.getImage();
|
cursorMiddle = GameImage.CURSOR_MIDDLE.getImage();
|
||||||
|
|
||||||
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
|
|
||||||
int removeCount = 0;
|
int removeCount = 0;
|
||||||
int FPSmod = (Options.getTargetFPS() / 60);
|
int FPSmod = (Options.getTargetFPS() / 60);
|
||||||
|
|
||||||
|
@ -226,10 +254,7 @@ public class UI {
|
||||||
|
|
||||||
// increase the cursor size if pressed
|
// increase the cursor size if pressed
|
||||||
final float scale = 1.25f;
|
final float scale = 1.25f;
|
||||||
int state = game.getCurrentStateID();
|
if (mousePressed) {
|
||||||
if (((state == Opsu.STATE_GAME || state == Opsu.STATE_GAMEPAUSEMENU) && Utils.isGameKeyPressed()) ||
|
|
||||||
((input.isMouseButtonDown(Input.MOUSE_LEFT_BUTTON) || input.isMouseButtonDown(Input.MOUSE_RIGHT_BUTTON)) &&
|
|
||||||
!(state == Opsu.STATE_GAME && Options.isMouseDisabled()))) {
|
|
||||||
cursor = cursor.getScaledCopy(scale);
|
cursor = cursor.getScaledCopy(scale);
|
||||||
if (newStyle)
|
if (newStyle)
|
||||||
cursorMiddle = cursorMiddle.getScaledCopy(scale);
|
cursorMiddle = cursorMiddle.getScaledCopy(scale);
|
||||||
|
|
|
@ -41,6 +41,7 @@ import java.util.Arrays;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Scanner;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
|
|
||||||
|
@ -546,4 +547,15 @@ public class Utils {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an input stream to a string.
|
||||||
|
* @param is the input stream
|
||||||
|
* @author Pavel Repin, earcam (http://stackoverflow.com/a/5445161)
|
||||||
|
*/
|
||||||
|
public static String convertStreamToString(InputStream is) {
|
||||||
|
try (Scanner s = new Scanner(is)) {
|
||||||
|
return s.useDelimiter("\\A").hasNext() ? s.next() : "";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -150,7 +150,7 @@ public class Circle implements HitObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean update(boolean overlap, int delta, int mouseX, int mouseY) {
|
public boolean update(boolean overlap, int delta, int mouseX, int mouseY, boolean keyPressed) {
|
||||||
int time = hitObject.getTime();
|
int time = hitObject.getTime();
|
||||||
float x = hitObject.getX(), y = hitObject.getY();
|
float x = hitObject.getX(), y = hitObject.getY();
|
||||||
|
|
||||||
|
|
|
@ -37,9 +37,10 @@ public interface HitObject {
|
||||||
* @param delta the delta interval since the last call
|
* @param delta the delta interval since the last call
|
||||||
* @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
|
||||||
* @return true if object ended
|
* @return true if object ended
|
||||||
*/
|
*/
|
||||||
public boolean update(boolean overlap, int delta, int mouseX, int mouseY);
|
public boolean update(boolean overlap, int delta, int mouseX, int mouseY, boolean keyPressed);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes a mouse click.
|
* Processes a mouse click.
|
||||||
|
|
|
@ -294,7 +294,7 @@ public class Slider implements HitObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean update(boolean overlap, int delta, int mouseX, int mouseY) {
|
public boolean update(boolean overlap, int delta, int mouseX, int mouseY, boolean keyPressed) {
|
||||||
int repeatCount = hitObject.getRepeatCount();
|
int repeatCount = hitObject.getRepeatCount();
|
||||||
|
|
||||||
// slider time and tick calculations
|
// slider time and tick calculations
|
||||||
|
@ -355,7 +355,7 @@ public class Slider implements HitObject {
|
||||||
tickIntervals++;
|
tickIntervals++;
|
||||||
|
|
||||||
// check if cursor pressed and within end circle
|
// check if cursor pressed and within end circle
|
||||||
if (Utils.isGameKeyPressed() || GameMod.RELAX.isActive()) {
|
if (keyPressed || GameMod.RELAX.isActive()) {
|
||||||
float[] c = curve.pointAt(getT(trackPosition, false));
|
float[] c = curve.pointAt(getT(trackPosition, false));
|
||||||
double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY);
|
double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY);
|
||||||
int followCircleRadius = GameImage.SLIDER_FOLLOWCIRCLE.getImage().getWidth() / 2;
|
int followCircleRadius = GameImage.SLIDER_FOLLOWCIRCLE.getImage().getWidth() / 2;
|
||||||
|
@ -404,7 +404,7 @@ public class Slider implements HitObject {
|
||||||
float[] c = curve.pointAt(getT(trackPosition, false));
|
float[] c = curve.pointAt(getT(trackPosition, false));
|
||||||
double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY);
|
double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY);
|
||||||
int followCircleRadius = GameImage.SLIDER_FOLLOWCIRCLE.getImage().getWidth() / 2;
|
int followCircleRadius = GameImage.SLIDER_FOLLOWCIRCLE.getImage().getWidth() / 2;
|
||||||
if (((Utils.isGameKeyPressed() || GameMod.RELAX.isActive()) && distance < followCircleRadius) || isAutoMod) {
|
if (((keyPressed || GameMod.RELAX.isActive()) && distance < followCircleRadius) || isAutoMod) {
|
||||||
// mouse pressed and within follow circle
|
// mouse pressed and within follow circle
|
||||||
followCircleActive = true;
|
followCircleActive = true;
|
||||||
data.changeHealth(delta * GameData.HP_DRAIN_MULTIPLIER);
|
data.changeHealth(delta * GameData.HP_DRAIN_MULTIPLIER);
|
||||||
|
|
|
@ -194,7 +194,7 @@ public class Spinner implements HitObject {
|
||||||
public boolean mousePressed(int x, int y) { return false; } // not used
|
public boolean mousePressed(int x, int y) { return false; } // not used
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean update(boolean overlap, int delta, int mouseX, int mouseY) {
|
public boolean update(boolean overlap, int delta, int mouseX, int mouseY, boolean keyPressed) {
|
||||||
int trackPosition = MusicController.getPosition();
|
int trackPosition = MusicController.getPosition();
|
||||||
|
|
||||||
// end of spinner
|
// end of spinner
|
||||||
|
@ -204,7 +204,7 @@ public class Spinner implements HitObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
// game button is released
|
// game button is released
|
||||||
if (isSpinning && !(Utils.isGameKeyPressed() || GameMod.RELAX.isActive()))
|
if (isSpinning && !(keyPressed || GameMod.RELAX.isActive()))
|
||||||
isSpinning = false;
|
isSpinning = false;
|
||||||
|
|
||||||
// spin automatically
|
// spin automatically
|
||||||
|
@ -224,7 +224,7 @@ public class Spinner implements HitObject {
|
||||||
angle = (float) Math.atan2(mouseY - (height / 2), mouseX - (width / 2));
|
angle = (float) Math.atan2(mouseY - (height / 2), mouseX - (width / 2));
|
||||||
|
|
||||||
// set initial angle to current mouse position to skip first click
|
// set initial angle to current mouse position to skip first click
|
||||||
if (!isSpinning && (Utils.isGameKeyPressed() || GameMod.RELAX.isActive())) {
|
if (!isSpinning && (keyPressed || GameMod.RELAX.isActive())) {
|
||||||
lastAngle = angle;
|
lastAngle = angle;
|
||||||
isSpinning = true;
|
isSpinning = true;
|
||||||
return false;
|
return false;
|
||||||
|
|
57
src/itdelatrisu/opsu/replay/LifeFrame.java
Normal file
57
src/itdelatrisu/opsu/replay/LifeFrame.java
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* opsu! - an open-source osu! client
|
||||||
|
* Copyright (C) 2014, 2015 Jeffrey Han
|
||||||
|
*
|
||||||
|
* opsu! is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* opsu! is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Captures a single life frame.
|
||||||
|
*
|
||||||
|
* @author smoogipooo (https://github.com/smoogipooo/osu-Replay-API/)
|
||||||
|
*/
|
||||||
|
package itdelatrisu.opsu.replay;
|
||||||
|
|
||||||
|
public class LifeFrame {
|
||||||
|
/** Time. */
|
||||||
|
private int time;
|
||||||
|
|
||||||
|
/** Percentage. */
|
||||||
|
private float percentage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
* @param time the time
|
||||||
|
* @param percentage the percentage
|
||||||
|
*/
|
||||||
|
public LifeFrame(int time, float percentage) {
|
||||||
|
this.time = time;
|
||||||
|
this.percentage = percentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the frame time.
|
||||||
|
*/
|
||||||
|
public int getTime() { return time; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the frame percentage.
|
||||||
|
*/
|
||||||
|
public float getPercentage() { return percentage; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("(%d, %.2f)", time, percentage);
|
||||||
|
}
|
||||||
|
}
|
221
src/itdelatrisu/opsu/replay/Replay.java
Normal file
221
src/itdelatrisu/opsu/replay/Replay.java
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
/*
|
||||||
|
* opsu! - an open-source osu! client
|
||||||
|
* Copyright (C) 2014, 2015 Jeffrey Han
|
||||||
|
*
|
||||||
|
* opsu! is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* opsu! is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package itdelatrisu.opsu.replay;
|
||||||
|
|
||||||
|
import itdelatrisu.opsu.OsuReader;
|
||||||
|
import itdelatrisu.opsu.Utils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream;
|
||||||
|
import org.newdawn.slick.util.Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Captures osu! replay data.
|
||||||
|
* https://osu.ppy.sh/wiki/Osr_%28file_format%29
|
||||||
|
*
|
||||||
|
* @author smoogipooo (https://github.com/smoogipooo/osu-Replay-API/)
|
||||||
|
*/
|
||||||
|
public class Replay {
|
||||||
|
/** The associated file. */
|
||||||
|
private File file;
|
||||||
|
|
||||||
|
/** The game mode. */
|
||||||
|
public byte mode;
|
||||||
|
|
||||||
|
/** Game version when the replay was created. */
|
||||||
|
public int version;
|
||||||
|
|
||||||
|
/** Beatmap MD5 hash. */
|
||||||
|
public String beatmapHash;
|
||||||
|
|
||||||
|
/** The player's name. */
|
||||||
|
public String playerName;
|
||||||
|
|
||||||
|
/** Replay MD5 hash. */
|
||||||
|
public String replayHash;
|
||||||
|
|
||||||
|
/** Hit result counts. */
|
||||||
|
public short hit300, hit100, hit50, geki, katu, miss;
|
||||||
|
|
||||||
|
/** The score. */
|
||||||
|
public long score;
|
||||||
|
|
||||||
|
/** The max combo. */
|
||||||
|
public short combo;
|
||||||
|
|
||||||
|
/** Whether or not a full combo was achieved. */
|
||||||
|
public boolean perfect;
|
||||||
|
|
||||||
|
/** Game mod bitmask. */
|
||||||
|
public int mods;
|
||||||
|
|
||||||
|
/** Life frames. */
|
||||||
|
public LifeFrame[] lifeFrames;
|
||||||
|
|
||||||
|
/** The time when the replay was created. */
|
||||||
|
public Date timestamp;
|
||||||
|
|
||||||
|
/** Length of the replay data. */
|
||||||
|
public int replayLength;
|
||||||
|
|
||||||
|
/** Replay frames. */
|
||||||
|
public ReplayFrame[] frames;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
* @param file the file to load from
|
||||||
|
*/
|
||||||
|
public Replay(File file) {
|
||||||
|
this.file = file;
|
||||||
|
try {
|
||||||
|
OsuReader reader = new OsuReader(file);
|
||||||
|
loadHeader(reader);
|
||||||
|
loadData(reader);
|
||||||
|
reader.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the replay header data.
|
||||||
|
* @param reader the associated reader
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
private void loadHeader(OsuReader reader) throws IOException {
|
||||||
|
this.mode = reader.readByte();
|
||||||
|
this.version = reader.readInt();
|
||||||
|
this.beatmapHash = reader.readString();
|
||||||
|
this.playerName = reader.readString();
|
||||||
|
this.replayHash = reader.readString();
|
||||||
|
this.hit300 = reader.readShort();
|
||||||
|
this.hit100 = reader.readShort();
|
||||||
|
this.hit50 = reader.readShort();
|
||||||
|
this.geki = reader.readShort();
|
||||||
|
this.katu = reader.readShort();
|
||||||
|
this.miss = reader.readShort();
|
||||||
|
this.score = reader.readInt();
|
||||||
|
this.combo = reader.readShort();
|
||||||
|
this.perfect = reader.readBoolean();
|
||||||
|
this.mods = reader.readInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the replay data.
|
||||||
|
* @param reader the associated reader
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
private void loadData(OsuReader reader) throws IOException {
|
||||||
|
// life data
|
||||||
|
String[] lifeData = reader.readString().split(",");
|
||||||
|
List<LifeFrame> lifeFrameList = new ArrayList<LifeFrame>(lifeData.length);
|
||||||
|
for (String frame : lifeData) {
|
||||||
|
String[] tokens = frame.split("\\|");
|
||||||
|
if (tokens.length < 2)
|
||||||
|
continue;
|
||||||
|
try {
|
||||||
|
int time = Integer.parseInt(tokens[0]);
|
||||||
|
float percentage = Float.parseFloat(tokens[1]);
|
||||||
|
lifeFrameList.add(new LifeFrame(time, percentage));
|
||||||
|
} catch (NumberFormatException | NullPointerException e) {
|
||||||
|
Log.warn(String.format("Failed to life frame: '%s'", frame), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.lifeFrames = lifeFrameList.toArray(new LifeFrame[lifeFrameList.size()]);
|
||||||
|
|
||||||
|
// timestamp
|
||||||
|
this.timestamp = reader.readDate();
|
||||||
|
|
||||||
|
// LZMA-encoded replay data
|
||||||
|
this.replayLength = reader.readInt();
|
||||||
|
if (replayLength > 0) {
|
||||||
|
LZMACompressorInputStream lzma = new LZMACompressorInputStream(reader.getInputStream());
|
||||||
|
String[] replayFrames = Utils.convertStreamToString(lzma).split(",");
|
||||||
|
List<ReplayFrame> replayFrameList = new ArrayList<ReplayFrame>(replayFrames.length);
|
||||||
|
int lastTime = 0;
|
||||||
|
for (String frame : replayFrames) {
|
||||||
|
if (frame.isEmpty())
|
||||||
|
continue;
|
||||||
|
String[] tokens = frame.split("\\|");
|
||||||
|
if (tokens.length < 4)
|
||||||
|
continue;
|
||||||
|
try {
|
||||||
|
int timeDiff = Integer.parseInt(tokens[0]);
|
||||||
|
int time = timeDiff + lastTime;
|
||||||
|
float x = Float.parseFloat(tokens[1]);
|
||||||
|
float y = Float.parseFloat(tokens[2]);
|
||||||
|
int keys = Integer.parseInt(tokens[3]);
|
||||||
|
replayFrameList.add(new ReplayFrame(timeDiff, time, x, y, keys));
|
||||||
|
lastTime = time;
|
||||||
|
} catch (NumberFormatException | NullPointerException e) {
|
||||||
|
Log.warn(String.format("Failed to parse frame: '%s'", frame), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.frames = replayFrameList.toArray(new ReplayFrame[replayFrameList.size()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
final int LINE_SPLIT = 10;
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("File: "); sb.append(file.getName()); sb.append('\n');
|
||||||
|
sb.append("Mode: "); sb.append(mode); sb.append('\n');
|
||||||
|
sb.append("Version: "); sb.append(version); sb.append('\n');
|
||||||
|
sb.append("Beatmap hash: "); sb.append(beatmapHash); sb.append('\n');
|
||||||
|
sb.append("Player name: "); sb.append(playerName); sb.append('\n');
|
||||||
|
sb.append("Replay hash: "); sb.append(replayHash); sb.append('\n');
|
||||||
|
sb.append("Hits: ");
|
||||||
|
sb.append(hit300); sb.append(' ');
|
||||||
|
sb.append(hit100); sb.append(' ');
|
||||||
|
sb.append(hit50); sb.append(' ');
|
||||||
|
sb.append(geki); sb.append(' ');
|
||||||
|
sb.append(katu); sb.append(' ');
|
||||||
|
sb.append(miss); sb.append('\n');
|
||||||
|
sb.append("Score: "); sb.append(score); sb.append('\n');
|
||||||
|
sb.append("Max combo: "); sb.append(combo); sb.append('\n');
|
||||||
|
sb.append("Perfect: "); sb.append(perfect); sb.append('\n');
|
||||||
|
sb.append("Mods: "); sb.append(mods); sb.append('\n');
|
||||||
|
sb.append("Life data:\n");
|
||||||
|
for (int i = 0; i < lifeFrames.length; i++) {
|
||||||
|
if (i % LINE_SPLIT == 0)
|
||||||
|
sb.append('\t');
|
||||||
|
sb.append(lifeFrames[i]);
|
||||||
|
sb.append((i % LINE_SPLIT == LINE_SPLIT - 1) ? '\n' : ' ');
|
||||||
|
}
|
||||||
|
sb.append('\n');
|
||||||
|
sb.append("Timestamp: "); sb.append(timestamp); sb.append('\n');
|
||||||
|
sb.append("Replay length: "); sb.append(replayLength); sb.append('\n');
|
||||||
|
if (frames != null) {
|
||||||
|
sb.append("Frames:\n");
|
||||||
|
for (int i = 0; i < frames.length; i++) {
|
||||||
|
if (i % LINE_SPLIT == 0)
|
||||||
|
sb.append('\t');
|
||||||
|
sb.append(frames[i]);
|
||||||
|
sb.append((i % LINE_SPLIT == LINE_SPLIT - 1) ? '\n' : ' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
99
src/itdelatrisu/opsu/replay/ReplayFrame.java
Normal file
99
src/itdelatrisu/opsu/replay/ReplayFrame.java
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
/*
|
||||||
|
* opsu! - an open-source osu! client
|
||||||
|
* Copyright (C) 2014, 2015 Jeffrey Han
|
||||||
|
*
|
||||||
|
* opsu! is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* opsu! is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package itdelatrisu.opsu.replay;
|
||||||
|
|
||||||
|
import itdelatrisu.opsu.OsuHitObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Captures a single replay frame.
|
||||||
|
*
|
||||||
|
* @author smoogipooo (https://github.com/smoogipooo/osu-Replay-API/)
|
||||||
|
*/
|
||||||
|
public class ReplayFrame {
|
||||||
|
/** Key bits. */
|
||||||
|
public static final int
|
||||||
|
KEY_NONE = 0,
|
||||||
|
KEY_M1 = (1 << 0),
|
||||||
|
KEY_M2 = (1 << 1),
|
||||||
|
KEY_K1 = (1 << 2) | (1 << 0),
|
||||||
|
KEY_K2 = (1 << 3) | (1 << 1);
|
||||||
|
|
||||||
|
/** Time, in milliseconds, since the previous action. */
|
||||||
|
private int timeDiff;
|
||||||
|
|
||||||
|
/** Time, in milliseconds. */
|
||||||
|
private int time;
|
||||||
|
|
||||||
|
/** Cursor coordinates (in OsuPixels). */
|
||||||
|
private float x, y;
|
||||||
|
|
||||||
|
/** Keys pressed (bitmask). */
|
||||||
|
private int keys;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
* @param timeDiff time since the previous action (in ms)
|
||||||
|
* @param time time (in ms)
|
||||||
|
* @param x cursor x coordinate [0, 512]
|
||||||
|
* @param y cursor y coordinate [0, 384]
|
||||||
|
* @param keys keys pressed (bitmask)
|
||||||
|
*/
|
||||||
|
public ReplayFrame(int timeDiff, int time, float x, float y, int keys) {
|
||||||
|
this.timeDiff = timeDiff;
|
||||||
|
this.time = time;
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.keys = keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the frame time, in milliseconds.
|
||||||
|
*/
|
||||||
|
public int getTime() { return time; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the time since the previous action, in milliseconds.
|
||||||
|
*/
|
||||||
|
public int getTimeDiff() { return timeDiff; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the scaled cursor x coordinate.
|
||||||
|
*/
|
||||||
|
public int getX() { return (int) (x * OsuHitObject.getXMultiplier() + OsuHitObject.getXOffset()); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the scaled cursor Y coordinate.
|
||||||
|
*/
|
||||||
|
public int getY() { return (int) (y * OsuHitObject.getYMultiplier() + OsuHitObject.getYOffset()); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the keys pressed (KEY_* bitmask).
|
||||||
|
*/
|
||||||
|
public int getKeys() { return keys; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether or not a key is pressed.
|
||||||
|
*/
|
||||||
|
public boolean isKeyPressed() { return (keys != KEY_NONE); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("(%d, [%.2f, %.2f], %d)", time, x, y, keys);
|
||||||
|
}
|
||||||
|
}
|
|
@ -40,6 +40,8 @@ import itdelatrisu.opsu.objects.Circle;
|
||||||
import itdelatrisu.opsu.objects.HitObject;
|
import itdelatrisu.opsu.objects.HitObject;
|
||||||
import itdelatrisu.opsu.objects.Slider;
|
import itdelatrisu.opsu.objects.Slider;
|
||||||
import itdelatrisu.opsu.objects.Spinner;
|
import itdelatrisu.opsu.objects.Spinner;
|
||||||
|
import itdelatrisu.opsu.replay.Replay;
|
||||||
|
import itdelatrisu.opsu.replay.ReplayFrame;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.Stack;
|
import java.util.Stack;
|
||||||
|
@ -146,6 +148,24 @@ public class Game extends BasicGameState {
|
||||||
/** Number of retries. */
|
/** Number of retries. */
|
||||||
private int retries = 0;
|
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
|
// game-related variables
|
||||||
private GameContainer container;
|
private GameContainer container;
|
||||||
private StateBasedGame game;
|
private StateBasedGame game;
|
||||||
|
@ -365,14 +385,24 @@ public class Game extends BasicGameState {
|
||||||
cursorCirclePulse.drawCentered(pausedMouseX, pausedMouseY);
|
cursorCirclePulse.drawCentered(pausedMouseX, pausedMouseY);
|
||||||
}
|
}
|
||||||
|
|
||||||
UI.draw(g);
|
if (replay == null)
|
||||||
|
UI.draw(g);
|
||||||
|
else
|
||||||
|
UI.draw(g, replayX, replayY, replayKeyPressed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void update(GameContainer container, StateBasedGame game, int delta)
|
public void update(GameContainer container, StateBasedGame game, int delta)
|
||||||
throws SlickException {
|
throws SlickException {
|
||||||
UI.update(delta);
|
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);
|
skipButton.hoverUpdate(delta, mouseX, mouseY);
|
||||||
|
|
||||||
if (isLeadIn()) { // stop updating during song lead-in
|
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)) {
|
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);
|
hitObjects[objectIndex].update(true, delta, mouseX, mouseY, false);
|
||||||
|
|
||||||
if (checkpointLoaded) // if checkpoint used, skip ranking screen
|
if (checkpointLoaded) // if checkpoint used, skip ranking screen
|
||||||
game.closeRequested();
|
game.closeRequested();
|
||||||
else { // go to ranking screen
|
else { // go to ranking screen
|
||||||
((GameRanking) game.getState(Opsu.STATE_GAMERANKING)).setGameData(data);
|
((GameRanking) game.getState(Opsu.STATE_GAMERANKING)).setGameData(data);
|
||||||
ScoreData score = data.getScoreData(osu);
|
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);
|
ScoreDB.addScore(score);
|
||||||
game.enterState(Opsu.STATE_GAMERANKING, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
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
|
// 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
|
||||||
|
@ -478,7 +534,7 @@ public class Game extends BasicGameState {
|
||||||
}
|
}
|
||||||
|
|
||||||
// pause game if focus lost
|
// pause game if focus lost
|
||||||
if (!container.hasFocus() && !GameMod.AUTO.isActive()) {
|
if (!container.hasFocus() && !GameMod.AUTO.isActive() && replay == null) {
|
||||||
if (pauseTime < 0) {
|
if (pauseTime < 0) {
|
||||||
pausedMouseX = mouseX;
|
pausedMouseX = mouseX;
|
||||||
pausedMouseY = mouseY;
|
pausedMouseY = mouseY;
|
||||||
|
@ -503,18 +559,21 @@ public class Game extends BasicGameState {
|
||||||
}
|
}
|
||||||
|
|
||||||
// game over, force a restart
|
// game over, force a restart
|
||||||
restart = Restart.LOSE;
|
if (replay == null) {
|
||||||
game.enterState(Opsu.STATE_GAMEPAUSEMENU);
|
restart = Restart.LOSE;
|
||||||
|
game.enterState(Opsu.STATE_GAMEPAUSEMENU);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// update objects (loop in unlikely event of any skipped indexes)
|
// 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()) {
|
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))
|
if (hitObjects[objectIndex].update(overlap, delta, mouseX, mouseY, keyPressed))
|
||||||
objectIndex++; // done, so increment object index
|
objectIndex++; // done, so increment object index
|
||||||
else
|
else
|
||||||
break;
|
break;
|
||||||
|
@ -529,7 +588,7 @@ public class Game extends BasicGameState {
|
||||||
int trackPosition = MusicController.getPosition();
|
int trackPosition = MusicController.getPosition();
|
||||||
|
|
||||||
// game keys
|
// game keys
|
||||||
if (!Keyboard.isRepeatEvent()) {
|
if (!Keyboard.isRepeatEvent() && replay == null) {
|
||||||
if (key == Options.getGameKeyLeft())
|
if (key == Options.getGameKeyLeft())
|
||||||
gameKeyPressed(Input.MOUSE_LEFT_BUTTON, input.getMouseX(), input.getMouseY());
|
gameKeyPressed(Input.MOUSE_LEFT_BUTTON, input.getMouseX(), input.getMouseY());
|
||||||
else if (key == Options.getGameKeyRight())
|
else if (key == Options.getGameKeyRight())
|
||||||
|
@ -538,8 +597,8 @@ public class Game extends BasicGameState {
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case Input.KEY_ESCAPE:
|
case Input.KEY_ESCAPE:
|
||||||
// "auto" mod: go back to song menu
|
// "auto" mod or watching replay: go back to song menu
|
||||||
if (GameMod.AUTO.isActive()) {
|
if (GameMod.AUTO.isActive() || replay != null) {
|
||||||
game.closeRequested();
|
game.closeRequested();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -636,6 +695,10 @@ public class Game extends BasicGameState {
|
||||||
if (Options.isMouseDisabled())
|
if (Options.isMouseDisabled())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// watching replay
|
||||||
|
if (replay != null)
|
||||||
|
return;
|
||||||
|
|
||||||
// mouse wheel: pause the game
|
// mouse wheel: pause the game
|
||||||
if (button == Input.MOUSE_MIDDLE_BUTTON && !Options.isMouseWheelDisabled()) {
|
if (button == Input.MOUSE_MIDDLE_BUTTON && !Options.isMouseWheelDisabled()) {
|
||||||
int trackPosition = MusicController.getPosition();
|
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;
|
leadInTime = osu.audioLeadIn + approachTime;
|
||||||
restart = Restart.FALSE;
|
restart = Restart.FALSE;
|
||||||
}
|
}
|
||||||
|
@ -776,11 +864,15 @@ public class Game extends BasicGameState {
|
||||||
skipButton.resetHover();
|
skipButton.resetHover();
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Override
|
@Override
|
||||||
// public void leave(GameContainer container, StateBasedGame game)
|
public void leave(GameContainer container, StateBasedGame game)
|
||||||
// throws SlickException {
|
throws SlickException {
|
||||||
// container.setMouseGrabbed(false);
|
// container.setMouseGrabbed(false);
|
||||||
// }
|
|
||||||
|
// reset previous mod state
|
||||||
|
if (replay != null)
|
||||||
|
GameMod.loadModState(previousMods);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets all game data and structures.
|
* Resets all game data and structures.
|
||||||
|
@ -816,12 +908,12 @@ public class Game extends BasicGameState {
|
||||||
private boolean skipIntro() {
|
private boolean skipIntro() {
|
||||||
int firstObjectTime = osu.objects[0].getTime();
|
int firstObjectTime = osu.objects[0].getTime();
|
||||||
int trackPosition = MusicController.getPosition();
|
int trackPosition = MusicController.getPosition();
|
||||||
if (objectIndex == 0 &&
|
if (objectIndex == 0 && trackPosition < firstObjectTime - SKIP_OFFSET) {
|
||||||
trackPosition < firstObjectTime - SKIP_OFFSET) {
|
|
||||||
if (isLeadIn()) {
|
if (isLeadIn()) {
|
||||||
leadInTime = 0;
|
leadInTime = 0;
|
||||||
MusicController.resume();
|
MusicController.resume();
|
||||||
}
|
}
|
||||||
|
replaySkipTime = -1;
|
||||||
MusicController.setPosition(firstObjectTime - SKIP_OFFSET);
|
MusicController.setPosition(firstObjectTime - SKIP_OFFSET);
|
||||||
SoundController.playSound(SoundEffect.MENUHIT);
|
SoundController.playSound(SoundEffect.MENUHIT);
|
||||||
return true;
|
return true;
|
||||||
|
@ -950,4 +1042,10 @@ public class Game extends BasicGameState {
|
||||||
* Returns the slider multiplier given by the current timing point.
|
* Returns the slider multiplier given by the current timing point.
|
||||||
*/
|
*/
|
||||||
public float getTimingPointMultiplier() { return beatLength / beatLengthBase; }
|
public float getTimingPointMultiplier() { return beatLength / beatLengthBase; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a replay to view.
|
||||||
|
* @param replay the replay
|
||||||
|
*/
|
||||||
|
public void setReplay(Replay replay) { this.replay = replay; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,7 @@ import itdelatrisu.opsu.audio.SoundController;
|
||||||
import itdelatrisu.opsu.audio.SoundEffect;
|
import itdelatrisu.opsu.audio.SoundEffect;
|
||||||
import itdelatrisu.opsu.db.OsuDB;
|
import itdelatrisu.opsu.db.OsuDB;
|
||||||
import itdelatrisu.opsu.db.ScoreDB;
|
import itdelatrisu.opsu.db.ScoreDB;
|
||||||
|
import itdelatrisu.opsu.replay.Replay;
|
||||||
import itdelatrisu.opsu.states.ButtonMenu.MenuState;
|
import itdelatrisu.opsu.states.ButtonMenu.MenuState;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user