Merge remote-tracking branch 'ita/master' into omaster
Conflicts: src/itdelatrisu/opsu/GameImage.java
This commit is contained in:
commit
be4cb6d608
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
/Replays/
|
||||
/Screenshots/
|
||||
/Skins/
|
||||
/SongPacks/
|
||||
|
|
10
pom.xml
10
pom.xml
|
@ -197,5 +197,15 @@
|
|||
<artifactId>maven-artifact</artifactId>
|
||||
<version>3.0.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-compress</artifactId>
|
||||
<version>1.8</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.jponge</groupId>
|
||||
<artifactId>lzma-java</artifactId>
|
||||
<version>1.2</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
|
BIN
res/pause-replay.png
Normal file
BIN
res/pause-replay.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
Before Width: | Height: | Size: 38 KiB |
Binary file not shown.
Before Width: | Height: | Size: 41 KiB |
|
@ -22,10 +22,15 @@ import itdelatrisu.opsu.audio.HitSound;
|
|||
import itdelatrisu.opsu.audio.MusicController;
|
||||
import itdelatrisu.opsu.audio.SoundController;
|
||||
import itdelatrisu.opsu.audio.SoundEffect;
|
||||
import itdelatrisu.opsu.downloads.Updater;
|
||||
import itdelatrisu.opsu.replay.Replay;
|
||||
import itdelatrisu.opsu.replay.ReplayFrame;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.concurrent.LinkedBlockingDeque;
|
||||
|
||||
import org.newdawn.slick.Animation;
|
||||
import org.newdawn.slick.Color;
|
||||
|
@ -165,7 +170,7 @@ public class GameData {
|
|||
private int[] hitResultOffset;
|
||||
|
||||
/** List of hit result objects associated with hit objects. */
|
||||
private LinkedList<OsuHitObjectResult> hitResultList;
|
||||
private LinkedBlockingDeque<OsuHitObjectResult> hitResultList;
|
||||
|
||||
/**
|
||||
* Class to store hit error information.
|
||||
|
@ -198,7 +203,7 @@ public class GameData {
|
|||
}
|
||||
|
||||
/** List containing recent hit error information. */
|
||||
private LinkedList<HitErrorInfo> hitErrorList;
|
||||
private LinkedBlockingDeque<HitErrorInfo> hitErrorList;
|
||||
|
||||
/**
|
||||
* Hit result helper class.
|
||||
|
@ -274,6 +279,9 @@ public class GameData {
|
|||
/** The associated score data. */
|
||||
private ScoreData scoreData;
|
||||
|
||||
/** The associated replay. */
|
||||
private Replay replay;
|
||||
|
||||
/** Whether this object is used for gameplay (true) or score viewing (false). */
|
||||
private boolean gameplay;
|
||||
|
||||
|
@ -318,6 +326,8 @@ public class GameData {
|
|||
hitResultCount[HIT_300K] = 0;
|
||||
hitResultCount[HIT_100K] = s.katu;
|
||||
hitResultCount[HIT_MISS] = s.miss;
|
||||
this.replay = (s.replayString == null) ? null :
|
||||
new Replay(new File(Options.getReplayDir(), String.format("%s.osr", s.replayString)));
|
||||
|
||||
loadImages();
|
||||
}
|
||||
|
@ -332,8 +342,8 @@ public class GameData {
|
|||
health = 100f;
|
||||
healthDisplay = 100f;
|
||||
hitResultCount = new int[HIT_MAX];
|
||||
hitResultList = new LinkedList<OsuHitObjectResult>();
|
||||
hitErrorList = new LinkedList<HitErrorInfo>();
|
||||
hitResultList = new LinkedBlockingDeque<OsuHitObjectResult>();
|
||||
hitErrorList = new LinkedBlockingDeque<HitErrorInfo>();
|
||||
fullObjectCount = 0;
|
||||
combo = 0;
|
||||
comboMax = 0;
|
||||
|
@ -1219,15 +1229,50 @@ public class GameData {
|
|||
scoreData.score = score;
|
||||
scoreData.combo = comboMax;
|
||||
scoreData.perfect = (comboMax == fullObjectCount);
|
||||
int mods = 0;
|
||||
for (GameMod mod : GameMod.values()) {
|
||||
if (mod.isActive())
|
||||
mods |= mod.getBit();
|
||||
}
|
||||
scoreData.mods = mods;
|
||||
scoreData.mods = GameMod.getModState();
|
||||
scoreData.replayString = (replay == null) ? null : replay.getReplayFilename();
|
||||
return scoreData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Replay object encapsulating all game data.
|
||||
* If a replay already exists and frames is null, the existing object will
|
||||
* be returned.
|
||||
* @param frames the replay frames
|
||||
* @return the Replay object, or null if none exists and frames is null
|
||||
*/
|
||||
public Replay getReplay(ReplayFrame[] frames) {
|
||||
if (replay != null && frames == null)
|
||||
return replay;
|
||||
|
||||
if (frames == null)
|
||||
return null;
|
||||
|
||||
replay = new Replay();
|
||||
replay.mode = OsuFile.MODE_OSU;
|
||||
replay.version = Updater.get().getBuildDate();
|
||||
replay.beatmapHash = ""; // TODO
|
||||
replay.playerName = ""; // TODO
|
||||
replay.replayHash = Long.toString(System.currentTimeMillis()); // TODO
|
||||
replay.hit300 = (short) hitResultCount[HIT_300];
|
||||
replay.hit100 = (short) hitResultCount[HIT_100];
|
||||
replay.hit50 = (short) hitResultCount[HIT_50];
|
||||
replay.geki = (short) hitResultCount[HIT_300G];
|
||||
replay.katu = (short) (hitResultCount[HIT_300K] + hitResultCount[HIT_100K]);
|
||||
replay.miss = (short) hitResultCount[HIT_MISS];
|
||||
replay.score = (int) score;
|
||||
replay.combo = (short) comboMax;
|
||||
replay.perfect = (comboMax == fullObjectCount);
|
||||
replay.mods = GameMod.getModState();
|
||||
replay.lifeFrames = null; // TODO
|
||||
replay.timestamp = new Date();
|
||||
replay.frames = frames;
|
||||
replay.seed = 0; // TODO
|
||||
replay.loaded = true;
|
||||
|
||||
return replay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not this object is used for gameplay.
|
||||
* @return true if gameplay, false if score viewing
|
||||
|
|
|
@ -87,6 +87,7 @@ public enum GameImage {
|
|||
PAUSE_CONTINUE ("pause-continue", "png"),
|
||||
PAUSE_RETRY ("pause-retry", "png"),
|
||||
PAUSE_BACK ("pause-back", "png"),
|
||||
PAUSE_REPLAY ("pause-replay", "png"),
|
||||
PAUSE_OVERLAY ("pause-overlay", "png|jpg") {
|
||||
@Override
|
||||
protected Image process_sub(Image img, int w, int h) {
|
||||
|
@ -289,8 +290,6 @@ public enum GameImage {
|
|||
MUSIC_PAUSE ("music-pause", "png", false, false),
|
||||
MUSIC_NEXT ("music-next", "png", false, false),
|
||||
MUSIC_PREVIOUS ("music-previous", "png", false, false),
|
||||
RANKING_RETRY ("ranking-retry", "png", false, false),
|
||||
RANKING_EXIT ("ranking-back", "png", false, false),
|
||||
DOWNLOADS ("downloads", "png", false, false) {
|
||||
@Override
|
||||
protected Image process_sub(Image img, int w, int h) {
|
||||
|
|
|
@ -213,6 +213,27 @@ public enum GameMod {
|
|||
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.
|
||||
* @param category the category for the mod
|
||||
|
|
|
@ -137,6 +137,10 @@ public class Opsu extends StateBasedGame {
|
|||
// initialize databases
|
||||
DBController.init();
|
||||
|
||||
// check if just updated
|
||||
if (args.length >= 2)
|
||||
Updater.get().setUpdateInfo(args[0], args[1]);
|
||||
|
||||
// check for updates
|
||||
new Thread() {
|
||||
@Override
|
||||
|
|
|
@ -90,6 +90,9 @@ public class Options {
|
|||
/** The screenshot directory (created when needed). */
|
||||
private static File screenshotDir;
|
||||
|
||||
/** The replay directory (created when needed). */
|
||||
private static File replayDir;
|
||||
|
||||
/** The current skin directory (for user skins). */
|
||||
private static File skinDir;
|
||||
|
||||
|
@ -819,6 +822,19 @@ public class Options {
|
|||
return screenshotDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the replay directory.
|
||||
* If invalid, this will return a "Replay" directory.
|
||||
* @return the replay directory
|
||||
*/
|
||||
public static File getReplayDir() {
|
||||
if (replayDir != null && replayDir.isDirectory())
|
||||
return replayDir;
|
||||
|
||||
replayDir = new File(DATA_DIR, "Replays/");
|
||||
return replayDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current skin directory.
|
||||
* If invalid, this will create a "Skins" folder in the root directory.
|
||||
|
@ -892,6 +908,9 @@ public class Options {
|
|||
case "ScreenshotDirectory":
|
||||
screenshotDir = new File(value);
|
||||
break;
|
||||
case "ReplayDirectory":
|
||||
replayDir = new File(value);
|
||||
break;
|
||||
case "Skin":
|
||||
skinDir = new File(value);
|
||||
break;
|
||||
|
@ -1054,6 +1073,8 @@ public class Options {
|
|||
writer.newLine();
|
||||
writer.write(String.format("ScreenshotDirectory = %s", getScreenshotDir().getAbsolutePath()));
|
||||
writer.newLine();
|
||||
writer.write(String.format("ReplayDirectory = %s", getReplayDir().getAbsolutePath()));
|
||||
writer.newLine();
|
||||
writer.write(String.format("Skin = %s", getSkinDir().getAbsolutePath()));
|
||||
writer.newLine();
|
||||
writer.write(String.format("ThemeSong = %s", themeString));
|
||||
|
|
|
@ -32,6 +32,9 @@ import org.newdawn.slick.util.Log;
|
|||
* Data type storing parsed data from OSU files.
|
||||
*/
|
||||
public class OsuFile implements Comparable<OsuFile> {
|
||||
/** Game modes. */
|
||||
public static final byte MODE_OSU = 0, MODE_TAIKO = 1, MODE_CTB = 2, MODE_MANIA = 3;
|
||||
|
||||
/** Map of all loaded background images. */
|
||||
private static HashMap<OsuFile, Image> bgImageMap = new HashMap<OsuFile, Image>();
|
||||
|
||||
|
@ -66,8 +69,8 @@ public class OsuFile implements Comparable<OsuFile> {
|
|||
/** How often closely placed hit objects will be stacked together. */
|
||||
public float stackLeniency = 0.7f;
|
||||
|
||||
/** Game mode (0:osu!, 1:taiko, 2:catch the beat, 3:osu!mania). */
|
||||
public byte mode = 0;
|
||||
/** Game mode (MODE_* constants). */
|
||||
public byte mode = MODE_OSU;
|
||||
|
||||
/** Whether the letterbox (top/bottom black bars) appears during breaks. */
|
||||
public boolean letterboxInBreaks = false;
|
||||
|
|
|
@ -59,7 +59,10 @@ public class OsuHitObject {
|
|||
xOffset, // offset right of border
|
||||
yOffset; // offset below health bar
|
||||
|
||||
/** Starting coordinates (scaled). */
|
||||
/** The container height. */
|
||||
private static int containerHeight;
|
||||
|
||||
/** Starting coordinates. */
|
||||
private float x, y;
|
||||
|
||||
/** Start time (in ms). */
|
||||
|
@ -77,7 +80,7 @@ public class OsuHitObject {
|
|||
/** Slider curve type (SLIDER_* constant). */
|
||||
private char sliderType;
|
||||
|
||||
/** Slider coordinate lists (scaled). */
|
||||
/** Slider coordinate lists. */
|
||||
private float[] sliderX, sliderY;
|
||||
|
||||
/** Slider repeat count. */
|
||||
|
@ -107,6 +110,7 @@ public class OsuHitObject {
|
|||
* @param height the container height
|
||||
*/
|
||||
public static void init(int width, int height) {
|
||||
containerHeight = height;
|
||||
int swidth = width;
|
||||
int sheight = height;
|
||||
if (swidth * 3 > sheight * 4)
|
||||
|
@ -129,6 +133,16 @@ public class OsuHitObject {
|
|||
*/
|
||||
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.
|
||||
* @param line the line to be parsed
|
||||
|
@ -153,8 +167,8 @@ public class OsuHitObject {
|
|||
String tokens[] = line.split(",");
|
||||
|
||||
// common fields
|
||||
this.x = Float.parseFloat(tokens[0]) * xMultiplier + xOffset;
|
||||
this.y = Float.parseFloat(tokens[1]) * yMultiplier + yOffset;
|
||||
this.x = Float.parseFloat(tokens[0]);
|
||||
this.y = Float.parseFloat(tokens[1]);
|
||||
this.time = Integer.parseInt(tokens[2]);
|
||||
this.type = Integer.parseInt(tokens[3]);
|
||||
this.hitSound = Byte.parseByte(tokens[4]);
|
||||
|
@ -175,8 +189,8 @@ public class OsuHitObject {
|
|||
this.sliderY = new float[sliderTokens.length - 1];
|
||||
for (int j = 1; j < sliderTokens.length; j++) {
|
||||
String[] sliderXY = sliderTokens[j].split(":");
|
||||
this.sliderX[j - 1] = Integer.parseInt(sliderXY[0]) * xMultiplier + xOffset;
|
||||
this.sliderY[j - 1] = Integer.parseInt(sliderXY[1]) * yMultiplier + yOffset;
|
||||
this.sliderX[j - 1] = Integer.parseInt(sliderXY[0]);
|
||||
this.sliderY[j - 1] = Integer.parseInt(sliderXY[1]);
|
||||
}
|
||||
this.repeat = Integer.parseInt(tokens[6]);
|
||||
this.pixelLength = Float.parseFloat(tokens[7]);
|
||||
|
@ -211,17 +225,30 @@ public class OsuHitObject {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the starting x coordinate.
|
||||
* @return the x coordinate
|
||||
* Returns the raw starting x coordinate.
|
||||
*/
|
||||
public float getX() { return x; }
|
||||
|
||||
/**
|
||||
* Returns the starting y coordinate.
|
||||
* @return the y coordinate
|
||||
* Returns the raw starting y coordinate.
|
||||
*/
|
||||
public float getY() { return y; }
|
||||
|
||||
/**
|
||||
* Returns the scaled starting x coordinate.
|
||||
*/
|
||||
public float getScaledX() { return x * xMultiplier + xOffset; }
|
||||
|
||||
/**
|
||||
* Returns the scaled starting y coordinate.
|
||||
*/
|
||||
public float getScaledY() {
|
||||
if (GameMod.HARD_ROCK.isActive())
|
||||
return containerHeight - (y * yMultiplier + yOffset);
|
||||
else
|
||||
return y * yMultiplier + yOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the start time.
|
||||
* @return the start time (in ms)
|
||||
|
@ -259,17 +286,48 @@ public class OsuHitObject {
|
|||
public char getSliderType() { return sliderType; }
|
||||
|
||||
/**
|
||||
* Returns a list of slider x coordinates.
|
||||
* @return the slider x coordinates
|
||||
* Returns a list of raw slider x coordinates.
|
||||
*/
|
||||
public float[] getSliderX() { return sliderX; }
|
||||
|
||||
/**
|
||||
* Returns a list of slider y coordinates.
|
||||
* @return the slider y coordinates
|
||||
* Returns a list of raw slider y coordinates.
|
||||
*/
|
||||
public float[] getSliderY() { return sliderY; }
|
||||
|
||||
/**
|
||||
* Returns a list of scaled slider x coordinates.
|
||||
* Note that this method will create a new array.
|
||||
*/
|
||||
public float[] getScaledSliderX() {
|
||||
if (sliderX == null)
|
||||
return null;
|
||||
|
||||
float[] x = new float[sliderX.length];
|
||||
for (int i = 0; i < x.length; i++)
|
||||
x[i] = sliderX[i] * xMultiplier + xOffset;
|
||||
return x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of scaled slider y coordinates.
|
||||
* Note that this method will create a new array.
|
||||
*/
|
||||
public float[] getScaledSliderY() {
|
||||
if (sliderY == null)
|
||||
return null;
|
||||
|
||||
float[] y = new float[sliderY.length];
|
||||
if (GameMod.HARD_ROCK.isActive()) {
|
||||
for (int i = 0; i < y.length; i++)
|
||||
y[i] = containerHeight - (sliderY[i] * yMultiplier + yOffset);
|
||||
} else {
|
||||
for (int i = 0; i < y.length; i++)
|
||||
y[i] = sliderY[i] * yMultiplier + yOffset;
|
||||
}
|
||||
return y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the slider repeat count.
|
||||
* @return the repeat count
|
||||
|
|
|
@ -272,7 +272,7 @@ public class OsuParser {
|
|||
osu.mode = Byte.parseByte(tokens[1]);
|
||||
|
||||
/* Non-Opsu! standard files not implemented (obviously). */
|
||||
if (osu.mode != 0)
|
||||
if (osu.mode != OsuFile.MODE_OSU)
|
||||
return null;
|
||||
|
||||
break;
|
||||
|
|
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);
|
||||
}
|
||||
}
|
162
src/itdelatrisu/opsu/OsuWriter.java
Normal file
162
src/itdelatrisu/opsu/OsuWriter.java
Normal file
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* 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.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* Writer for osu! file types.
|
||||
*/
|
||||
public class OsuWriter {
|
||||
/** Output stream writer. */
|
||||
private DataOutputStream writer;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param file the file to write to
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
public OsuWriter(File file) throws FileNotFoundException {
|
||||
this(new FileOutputStream(file));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param dest the output stream to write to
|
||||
*/
|
||||
public OsuWriter(OutputStream dest) {
|
||||
this.writer = new DataOutputStream(dest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the output stream in use.
|
||||
*/
|
||||
public OutputStream getOutputStream() { return writer; }
|
||||
|
||||
/**
|
||||
* Closes the output stream.
|
||||
* @throws IOException
|
||||
*/
|
||||
public void close() throws IOException { writer.close(); }
|
||||
|
||||
/**
|
||||
* Writes a 1-byte value.
|
||||
*/
|
||||
public void write(byte v) throws IOException { writer.writeByte(v); }
|
||||
|
||||
/**
|
||||
* Writes a 2-byte value.
|
||||
*/
|
||||
public void write(short v) throws IOException {
|
||||
byte[] bytes = ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort(v).array();
|
||||
writer.write(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a 4-byte value.
|
||||
*/
|
||||
public void write(int v) throws IOException {
|
||||
byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(v).array();
|
||||
writer.write(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an 8-byte value.
|
||||
*/
|
||||
public void write(long v) throws IOException {
|
||||
byte[] bytes = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(v).array();
|
||||
writer.write(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a 4-byte float.
|
||||
*/
|
||||
public void write(float v) throws IOException { writer.writeFloat(v); }
|
||||
|
||||
/**
|
||||
* Writes an 8-byte double.
|
||||
*/
|
||||
public void write(double v) throws IOException { writer.writeDouble(v); }
|
||||
|
||||
/**
|
||||
* Writes a boolean as a 1-byte value.
|
||||
*/
|
||||
public void write(boolean v) throws IOException { writer.writeBoolean(v); }
|
||||
|
||||
/**
|
||||
* Writes an unsigned variable length integer (ULEB128).
|
||||
*/
|
||||
public void writeULEB128(int i) throws IOException {
|
||||
int value = i;
|
||||
do {
|
||||
byte b = (byte) (value & 0x7F);
|
||||
value >>= 7;
|
||||
if (value != 0)
|
||||
b |= (1 << 7);
|
||||
writer.writeByte(b);
|
||||
} while (value != 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a variable-length string of 1-byte characters.
|
||||
*/
|
||||
public void write(String s) 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.
|
||||
if (s == null || s.length() == 0)
|
||||
writer.writeByte(0x00);
|
||||
else {
|
||||
writer.writeByte(0x0B);
|
||||
writeULEB128(s.length());
|
||||
writer.writeBytes(s);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a date in Windows ticks (8 bytes).
|
||||
*/
|
||||
public void write(Date date) throws IOException {
|
||||
final long TICKS_AT_EPOCH = 621355968000000000L;
|
||||
final long TICKS_PER_MILLISECOND = 10000;
|
||||
|
||||
Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
|
||||
calendar.setTime(date);
|
||||
long ticks = TICKS_AT_EPOCH + calendar.getTimeInMillis() * TICKS_PER_MILLISECOND;
|
||||
write(ticks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an array of bytes.
|
||||
*/
|
||||
public void write(byte[] b) throws IOException {
|
||||
writer.write(b);
|
||||
}
|
||||
}
|
|
@ -59,6 +59,9 @@ public class ScoreData implements Comparable<ScoreData> {
|
|||
/** Game mod bitmask. */
|
||||
public int mods;
|
||||
|
||||
/** The replay string. */
|
||||
public String replayString;
|
||||
|
||||
/** Time since the score was achieved. */
|
||||
private String timeSince;
|
||||
|
||||
|
@ -153,6 +156,7 @@ public class ScoreData implements Comparable<ScoreData> {
|
|||
this.combo = rs.getInt(15);
|
||||
this.perfect = rs.getBoolean(16);
|
||||
this.mods = rs.getInt(17);
|
||||
this.replayString = rs.getString(18);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -20,12 +20,16 @@ package itdelatrisu.opsu;
|
|||
|
||||
import itdelatrisu.opsu.audio.SoundController;
|
||||
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.UIManager;
|
||||
|
||||
import org.lwjgl.BufferUtils;
|
||||
import org.lwjgl.LWJGLException;
|
||||
import org.lwjgl.input.Cursor;
|
||||
import org.newdawn.slick.Animation;
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.GameContainer;
|
||||
|
@ -42,6 +46,9 @@ public class UI {
|
|||
/** Back button. */
|
||||
private static MenuButton backButton;
|
||||
|
||||
/** Empty cursor. */
|
||||
private static Cursor emptyCursor;
|
||||
|
||||
/** Last cursor coordinates. */
|
||||
private static int lastX = -1, lastY = -1;
|
||||
|
||||
|
@ -88,6 +95,16 @@ public class UI {
|
|||
UI.game = game;
|
||||
UI.input = container.getInput();
|
||||
|
||||
// hide native cursor
|
||||
try {
|
||||
int min = Cursor.getMinCursorSize();
|
||||
IntBuffer tmp = BufferUtils.createIntBuffer(min * min);
|
||||
emptyCursor = new Cursor(min, min, min/2, min/2, 1, tmp, null);
|
||||
hideCursor();
|
||||
} catch (LWJGLException e) {
|
||||
ErrorHandler.error("Failed to create hidden cursor.", e, true);
|
||||
}
|
||||
|
||||
// back button
|
||||
if (GameImage.MENU_BACK.getImages() != null) {
|
||||
Animation back = GameImage.MENU_BACK.getAnimation(120);
|
||||
|
@ -120,6 +137,20 @@ public class UI {
|
|||
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.
|
||||
*/
|
||||
|
@ -161,6 +192,21 @@ public class UI {
|
|||
* Draws the cursor.
|
||||
*/
|
||||
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
|
||||
// TODO: most beatmaps don't skin CURSOR_MIDDLE, so how to determine style?
|
||||
Image cursor = null, cursorMiddle = null, cursorTrail = null;
|
||||
|
@ -176,7 +222,6 @@ public class UI {
|
|||
if (newStyle)
|
||||
cursorMiddle = GameImage.CURSOR_MIDDLE.getImage();
|
||||
|
||||
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
|
||||
int removeCount = 0;
|
||||
int FPSmod = (Options.getTargetFPS() / 60);
|
||||
|
||||
|
@ -226,10 +271,7 @@ public class UI {
|
|||
|
||||
// increase the cursor size if pressed
|
||||
final float scale = 1.25f;
|
||||
int state = game.getCurrentStateID();
|
||||
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()))) {
|
||||
if (mousePressed) {
|
||||
cursor = cursor.getScaledCopy(scale);
|
||||
if (newStyle)
|
||||
cursorMiddle = cursorMiddle.getScaledCopy(scale);
|
||||
|
@ -318,6 +360,26 @@ public class UI {
|
|||
GameImage.CURSOR.getImage().setRotation(0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the cursor, if possible.
|
||||
*/
|
||||
public static void hideCursor() {
|
||||
if (emptyCursor != null) {
|
||||
try {
|
||||
container.setMouseCursor(emptyCursor, 0, 0);
|
||||
} catch (SlickException e) {
|
||||
ErrorHandler.error("Failed to hide the cursor.", e, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unhides the cursor.
|
||||
*/
|
||||
public static void showCursor() {
|
||||
container.setDefaultMouseCursor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the FPS at the bottom-right corner of the game container.
|
||||
* If the option is not activated, this will do nothing.
|
||||
|
|
|
@ -34,19 +34,17 @@ import java.net.HttpURLConnection;
|
|||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import org.lwjgl.BufferUtils;
|
||||
import org.lwjgl.LWJGLException;
|
||||
import org.lwjgl.input.Cursor;
|
||||
import org.lwjgl.opengl.Display;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.newdawn.slick.Animation;
|
||||
|
@ -130,6 +128,8 @@ public class Utils {
|
|||
public static void init(GameContainer container, StateBasedGame game)
|
||||
throws SlickException {
|
||||
input = container.getInput();
|
||||
int width = container.getWidth();
|
||||
int height = container.getHeight();
|
||||
|
||||
// game settings
|
||||
container.setTargetFrameRate(Options.getTargetFPS());
|
||||
|
@ -140,20 +140,6 @@ public class Utils {
|
|||
container.setAlwaysRender(true);
|
||||
container.setUpdateOnlyWhenVisible(false);
|
||||
|
||||
int width = container.getWidth();
|
||||
int height = container.getHeight();
|
||||
|
||||
// set the cursor
|
||||
try {
|
||||
// hide the native cursor
|
||||
int min = Cursor.getMinCursorSize();
|
||||
IntBuffer tmp = BufferUtils.createIntBuffer(min * min);
|
||||
Cursor emptyCursor = new Cursor(min, min, min/2, min/2, 1, tmp, null);
|
||||
container.setMouseCursor(emptyCursor, 0, 0);
|
||||
} catch (LWJGLException e) {
|
||||
ErrorHandler.error("Failed to set the cursor.", e, true);
|
||||
}
|
||||
|
||||
GameImage.init(width, height);
|
||||
|
||||
// create fonts
|
||||
|
@ -539,4 +525,15 @@ public class Utils {
|
|||
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() : "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -127,6 +127,10 @@ public class OsuDB {
|
|||
"PRAGMA locking_mode = EXCLUSIVE; " +
|
||||
"PRAGMA journal_mode = WAL;";
|
||||
stmt.executeUpdate(sql);
|
||||
|
||||
// set the version key, if empty
|
||||
sql = String.format("INSERT OR IGNORE INTO info(key, value) VALUES('version', '%s')", DATABASE_VERSION);
|
||||
stmt.executeUpdate(sql);
|
||||
} catch (SQLException e) {
|
||||
ErrorHandler.error("Could not create beatmap database.", e, true);
|
||||
}
|
||||
|
@ -145,14 +149,15 @@ public class OsuDB {
|
|||
rs.close();
|
||||
|
||||
// if different from current version, clear the database
|
||||
if (!version.equals(DATABASE_VERSION))
|
||||
if (!version.equals(DATABASE_VERSION)) {
|
||||
clearDatabase();
|
||||
|
||||
// update version
|
||||
PreparedStatement ps = connection.prepareStatement("REPLACE INTO info (key, value) VALUES ('version', ?)");
|
||||
ps.setString(1, DATABASE_VERSION);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
// update version
|
||||
PreparedStatement ps = connection.prepareStatement("REPLACE INTO info (key, value) VALUES ('version', ?)");
|
||||
ps.setString(1, DATABASE_VERSION);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
ErrorHandler.error("Beatmap database version checks failed.", e, true);
|
||||
}
|
||||
|
@ -428,7 +433,7 @@ public class OsuDB {
|
|||
* Sets all OsuFile non-array fields using a given result set.
|
||||
* @param rs the result set containing the fields
|
||||
* @param osu the OsuFile
|
||||
* @throws SQLException
|
||||
* @throws SQLException
|
||||
*/
|
||||
private static void setOsuFileFields(ResultSet rs, OsuFile osu) throws SQLException {
|
||||
try {
|
||||
|
@ -476,7 +481,7 @@ public class OsuDB {
|
|||
* Sets all OsuFile array fields using a given result set.
|
||||
* @param rs the result set containing the fields
|
||||
* @param osu the OsuFile
|
||||
* @throws SQLException
|
||||
* @throws SQLException
|
||||
*/
|
||||
private static void setOsuFileArrayFields(ResultSet rs, OsuFile osu) throws SQLException {
|
||||
try {
|
||||
|
|
|
@ -32,6 +32,7 @@ import java.util.ArrayList;
|
|||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
@ -39,6 +40,29 @@ import java.util.Map;
|
|||
* Handles connections and queries with the scores database.
|
||||
*/
|
||||
public class ScoreDB {
|
||||
/**
|
||||
* Current database version.
|
||||
* This value should be changed whenever the database format changes.
|
||||
* Add any update queries to the {@link #getUpdateQueries(int)} method.
|
||||
*/
|
||||
private static final int DATABASE_VERSION = 20140311;
|
||||
|
||||
/**
|
||||
* Returns a list of SQL queries to apply, in order, to update from
|
||||
* the given database version to the latest version.
|
||||
* @param version the current version
|
||||
* @return a list of SQL queries
|
||||
*/
|
||||
private static List<String> getUpdateQueries(int version) {
|
||||
List<String> list = new LinkedList<String>();
|
||||
if (version < 20140311)
|
||||
list.add("ALTER TABLE scores ADD COLUMN replay TEXT");
|
||||
|
||||
/* add future updates here */
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/** Database connection. */
|
||||
private static Connection connection;
|
||||
|
||||
|
@ -63,13 +87,16 @@ public class ScoreDB {
|
|||
if (connection == null)
|
||||
return;
|
||||
|
||||
// run any database updates
|
||||
updateDatabase();
|
||||
|
||||
// create the database
|
||||
createDatabase();
|
||||
|
||||
// prepare sql statements
|
||||
try {
|
||||
insertStmt = connection.prepareStatement(
|
||||
"INSERT INTO scores VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
"INSERT INTO scores VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
selectMapStmt = connection.prepareStatement(
|
||||
"SELECT * FROM scores WHERE " +
|
||||
|
@ -109,15 +136,75 @@ public class ScoreDB {
|
|||
"score INTEGER, " +
|
||||
"combo INTEGER, " +
|
||||
"perfect BOOLEAN, " +
|
||||
"mods INTEGER" +
|
||||
"mods INTEGER, " +
|
||||
"replay TEXT" +
|
||||
");" +
|
||||
"CREATE TABLE IF NOT EXISTS info (" +
|
||||
"key TEXT NOT NULL UNIQUE, value TEXT" +
|
||||
"); " +
|
||||
"CREATE INDEX IF NOT EXISTS idx ON scores (MID, MSID, title, artist, creator, version);";
|
||||
stmt.executeUpdate(sql);
|
||||
|
||||
// set the version key, if empty
|
||||
sql = String.format("INSERT OR IGNORE INTO info(key, value) VALUES('version', %d)", DATABASE_VERSION);
|
||||
stmt.executeUpdate(sql);
|
||||
} catch (SQLException e) {
|
||||
ErrorHandler.error("Could not create score database.", e, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies any database updates by comparing the current version to the
|
||||
* stored version. Does nothing if tables have not been created.
|
||||
*/
|
||||
private static void updateDatabase() {
|
||||
try (Statement stmt = connection.createStatement()) {
|
||||
int version = 0;
|
||||
|
||||
// if 'info' table does not exist, assume version 0 and apply all updates
|
||||
String sql = "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'info'";
|
||||
ResultSet rs = stmt.executeQuery(sql);
|
||||
boolean infoExists = rs.isBeforeFirst();
|
||||
rs.close();
|
||||
if (!infoExists) {
|
||||
// if 'scores' table also does not exist, databases not yet created
|
||||
sql = "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'scores'";
|
||||
ResultSet scoresRS = stmt.executeQuery(sql);
|
||||
boolean scoresExists = scoresRS.isBeforeFirst();
|
||||
scoresRS.close();
|
||||
if (!scoresExists)
|
||||
return;
|
||||
} else {
|
||||
// try to retrieve stored version
|
||||
sql = "SELECT value FROM info WHERE key = 'version'";
|
||||
ResultSet versionRS = stmt.executeQuery(sql);
|
||||
String versionString = (versionRS.next()) ? versionRS.getString(1) : "0";
|
||||
versionRS.close();
|
||||
try {
|
||||
version = Integer.parseInt(versionString);
|
||||
} catch (NumberFormatException e) {}
|
||||
}
|
||||
|
||||
// database versions match
|
||||
if (version >= DATABASE_VERSION)
|
||||
return;
|
||||
|
||||
// apply updates
|
||||
for (String query : getUpdateQueries(version))
|
||||
stmt.executeUpdate(query);
|
||||
|
||||
// update version
|
||||
if (infoExists) {
|
||||
PreparedStatement ps = connection.prepareStatement("REPLACE INTO info (key, value) VALUES ('version', ?)");
|
||||
ps.setString(1, Integer.toString(DATABASE_VERSION));
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
ErrorHandler.error("Failed to update score database.", e, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the game score to the database.
|
||||
* @param data the GameData object
|
||||
|
@ -128,6 +215,7 @@ public class ScoreDB {
|
|||
|
||||
try {
|
||||
setStatementFields(insertStmt, data);
|
||||
insertStmt.setString(18, data.replayString);
|
||||
insertStmt.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
ErrorHandler.error("Failed to save score to database.", e, true);
|
||||
|
|
|
@ -28,6 +28,10 @@ import java.io.File;
|
|||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.net.URL;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
|
||||
|
@ -93,6 +97,12 @@ public class Updater {
|
|||
/** The current and latest versions. */
|
||||
private DefaultArtifactVersion currentVersion, latestVersion;
|
||||
|
||||
/** The version information if the program was just updated. */
|
||||
private String updatedFromVersion, updatedToVersion;
|
||||
|
||||
/** The build date. */
|
||||
private int buildDate = -1;
|
||||
|
||||
/** The download object. */
|
||||
private Download download;
|
||||
|
||||
|
@ -115,6 +125,57 @@ public class Updater {
|
|||
return (status == Status.UPDATE_AVAILABLE || status == Status.UPDATE_DOWNLOADED || status == Status.UPDATE_DOWNLOADING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the build date, or the current date if not available.
|
||||
*/
|
||||
public int getBuildDate() {
|
||||
if (buildDate == -1) {
|
||||
Date date = null;
|
||||
try {
|
||||
Properties props = new Properties();
|
||||
props.load(ResourceLoader.getResourceAsStream(Options.VERSION_FILE));
|
||||
String build = props.getProperty("build.date");
|
||||
if (build == null || build.equals("${timestamp}") || build.equals("${maven.build.timestamp}"))
|
||||
date = new Date();
|
||||
else {
|
||||
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.ENGLISH);
|
||||
date = format.parse(build);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
date = new Date();
|
||||
} finally {
|
||||
DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
|
||||
buildDate = Integer.parseInt(dateFormat.format(date));
|
||||
}
|
||||
}
|
||||
return buildDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the version information if the program was just updated.
|
||||
* @param fromVersion the previous version
|
||||
* @param toVersion the new version
|
||||
*/
|
||||
public void setUpdateInfo(String fromVersion, String toVersion) {
|
||||
this.updatedFromVersion = fromVersion;
|
||||
this.updatedToVersion = toVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the program was just updated.
|
||||
*/
|
||||
public boolean justUpdated() { return (updatedFromVersion != null && updatedToVersion != null); }
|
||||
|
||||
/**
|
||||
* Returns the version the program was just updated from, or null if not updated.
|
||||
*/
|
||||
public String updatedFromVersion() { return (justUpdated()) ? updatedFromVersion : null; }
|
||||
|
||||
/**
|
||||
* Returns the version the program was just updated to, or null if not updated.
|
||||
*/
|
||||
public String updatedToVersion() { return (justUpdated()) ? updatedToVersion : null; }
|
||||
|
||||
/**
|
||||
* Returns the version from a set of properties.
|
||||
* @param props the set of properties
|
||||
|
@ -215,7 +276,10 @@ public class Updater {
|
|||
|
||||
try {
|
||||
// TODO: it is better to wait for the process? is this portable?
|
||||
ProcessBuilder pb = new ProcessBuilder("java", "-jar", download.getLocalPath());
|
||||
ProcessBuilder pb = new ProcessBuilder(
|
||||
"java", "-jar", download.getLocalPath(),
|
||||
currentVersion.toString(), latestVersion.toString()
|
||||
);
|
||||
pb.start();
|
||||
} catch (IOException e) {
|
||||
status = Status.INTERNAL_ERROR;
|
||||
|
|
|
@ -37,6 +37,9 @@ public class Circle implements HitObject {
|
|||
/** The associated OsuHitObject. */
|
||||
private OsuHitObject hitObject;
|
||||
|
||||
/** The scaled starting x, y coordinates. */
|
||||
private float x, y;
|
||||
|
||||
/** The associated Game object. */
|
||||
private Game game;
|
||||
|
||||
|
@ -72,6 +75,8 @@ public class Circle implements HitObject {
|
|||
*/
|
||||
public Circle(OsuHitObject hitObject, Game game, GameData data, Color color, boolean comboEnd) {
|
||||
this.hitObject = hitObject;
|
||||
this.x = hitObject.getScaledX();
|
||||
this.y = hitObject.getScaledY();
|
||||
this.game = game;
|
||||
this.data = data;
|
||||
this.color = color;
|
||||
|
@ -84,7 +89,6 @@ public class Circle implements HitObject {
|
|||
|
||||
if (timeDiff >= 0) {
|
||||
float oldAlpha = color.a;
|
||||
float x = hitObject.getX(), y = hitObject.getY();
|
||||
float scale = timeDiff / (float)game.getApproachTime();
|
||||
|
||||
float approachScale = 1 + scale * 3;
|
||||
|
@ -129,7 +133,7 @@ public class Circle implements HitObject {
|
|||
|
||||
@Override
|
||||
public boolean mousePressed(int x, int y) {
|
||||
double distance = Math.hypot(hitObject.getX() - x, hitObject.getY() - y);
|
||||
double distance = Math.hypot(this.x - x, this.y - y);
|
||||
int circleRadius = GameImage.HITCIRCLE.getImage().getWidth() / 2;
|
||||
if (distance < circleRadius) {
|
||||
int trackPosition = MusicController.getPosition();
|
||||
|
@ -138,11 +142,7 @@ public class Circle implements HitObject {
|
|||
|
||||
if (result > -1) {
|
||||
data.addHitError(hitObject.getTime(), x, y, timeDiff);
|
||||
data.hitResult(
|
||||
hitObject.getTime(), result,
|
||||
hitObject.getX(), hitObject.getY(),
|
||||
color, comboEnd, hitObject, 0
|
||||
);
|
||||
data.hitResult(hitObject.getTime(), result, this.x, this.y, color, comboEnd, hitObject, 0);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -150,9 +150,8 @@ public class Circle implements HitObject {
|
|||
}
|
||||
|
||||
@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();
|
||||
float x = hitObject.getX(), y = hitObject.getY();
|
||||
|
||||
int trackPosition = MusicController.getPosition();
|
||||
int[] hitResultOffset = game.getHitResultOffsets();
|
||||
|
|
|
@ -37,9 +37,10 @@ public interface HitObject {
|
|||
* @param delta the delta interval since the last call
|
||||
* @param mouseX the x 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
|
||||
*/
|
||||
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.
|
||||
|
|
|
@ -52,6 +52,9 @@ public class Slider implements HitObject {
|
|||
/** The associated OsuHitObject. */
|
||||
private OsuHitObject hitObject;
|
||||
|
||||
/** The scaled starting x, y coordinates. */
|
||||
protected float x, y;
|
||||
|
||||
/** The associated Game object. */
|
||||
private Game game;
|
||||
|
||||
|
@ -130,6 +133,8 @@ public class Slider implements HitObject {
|
|||
*/
|
||||
public Slider(OsuHitObject hitObject, Game game, GameData data, Color color, boolean comboEnd) {
|
||||
this.hitObject = hitObject;
|
||||
this.x = hitObject.getScaledX();
|
||||
this.y = hitObject.getScaledY();
|
||||
this.game = game;
|
||||
this.data = data;
|
||||
this.color = color;
|
||||
|
@ -147,7 +152,6 @@ public class Slider implements HitObject {
|
|||
int timeDiff = hitObject.getTime() - trackPosition;
|
||||
float scale = timeDiff / (float) game.getApproachTime();
|
||||
float approachScale = 1 + scale * 3;
|
||||
float x = hitObject.getX(), y = hitObject.getY();
|
||||
float alpha = Utils.clamp(1 - scale, 0, 1);
|
||||
|
||||
float oldAlpha = color.a;
|
||||
|
@ -256,7 +260,7 @@ public class Slider implements HitObject {
|
|||
lastPos[0], lastPos[1], color, comboEnd, hitObject, currentRepeats + 1);
|
||||
} else { // first circle
|
||||
data.hitResult(hitObject.getTime() + (int) sliderTimeTotal, result,
|
||||
hitObject.getX(), hitObject.getY(), color, comboEnd, hitObject, currentRepeats + 1);
|
||||
x, y, color, comboEnd, hitObject, currentRepeats + 1);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
@ -267,7 +271,7 @@ public class Slider implements HitObject {
|
|||
if (sliderClickedInitial) // first circle already processed
|
||||
return false;
|
||||
|
||||
double distance = Math.hypot(hitObject.getX() - x, hitObject.getY() - y);
|
||||
double distance = Math.hypot(this.x - x, this.y - y);
|
||||
int circleRadius = GameImage.HITCIRCLE.getImage().getWidth() / 2;
|
||||
if (distance < circleRadius) {
|
||||
int trackPosition = MusicController.getPosition();
|
||||
|
@ -285,8 +289,7 @@ public class Slider implements HitObject {
|
|||
if (result > -1) {
|
||||
data.addHitError(hitObject.getTime(), x,y,trackPosition - hitObject.getTime());
|
||||
sliderClickedInitial = true;
|
||||
data.sliderTickResult(hitObject.getTime(), result,
|
||||
hitObject.getX(), hitObject.getY(), hitObject, currentRepeats);
|
||||
data.sliderTickResult(hitObject.getTime(), result, this.x, this.y, hitObject, currentRepeats);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -294,7 +297,7 @@ public class Slider implements HitObject {
|
|||
}
|
||||
|
||||
@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();
|
||||
|
||||
// slider time and tick calculations
|
||||
|
@ -317,7 +320,6 @@ public class Slider implements HitObject {
|
|||
|
||||
int trackPosition = MusicController.getPosition();
|
||||
int[] hitResultOffset = game.getHitResultOffsets();
|
||||
int lastIndex = hitObject.getSliderX().length - 1;
|
||||
boolean isAutoMod = GameMod.AUTO.isActive();
|
||||
|
||||
if (!sliderClickedInitial) {
|
||||
|
@ -328,11 +330,9 @@ public class Slider implements HitObject {
|
|||
sliderClickedInitial = true;
|
||||
if (isAutoMod) { // "auto" mod: catch any missed notes due to lag
|
||||
ticksHit++;
|
||||
data.sliderTickResult(time, GameData.HIT_SLIDER30,
|
||||
hitObject.getX(), hitObject.getY(), hitObject, currentRepeats);
|
||||
data.sliderTickResult(time, GameData.HIT_SLIDER30, x, y, hitObject, currentRepeats);
|
||||
} else
|
||||
data.sliderTickResult(time, GameData.HIT_MISS,
|
||||
hitObject.getX(), hitObject.getY(), hitObject, currentRepeats);
|
||||
data.sliderTickResult(time, GameData.HIT_MISS, x, y, hitObject, currentRepeats);
|
||||
}
|
||||
|
||||
// "auto" mod: send a perfect hit result
|
||||
|
@ -340,8 +340,7 @@ public class Slider implements HitObject {
|
|||
if (Math.abs(trackPosition - time) < hitResultOffset[GameData.HIT_300]) {
|
||||
ticksHit++;
|
||||
sliderClickedInitial = true;
|
||||
data.sliderTickResult(time, GameData.HIT_SLIDER30,
|
||||
hitObject.getX(), hitObject.getY(), hitObject, currentRepeats);
|
||||
data.sliderTickResult(time, GameData.HIT_SLIDER30, x, y, hitObject, currentRepeats);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -355,7 +354,7 @@ public class Slider implements HitObject {
|
|||
tickIntervals++;
|
||||
|
||||
// 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));
|
||||
double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY);
|
||||
int followCircleRadius = GameImage.SLIDER_FOLLOWCIRCLE.getImage().getWidth() / 2;
|
||||
|
@ -404,7 +403,7 @@ public class Slider implements HitObject {
|
|||
float[] c = curve.pointAt(getT(trackPosition, false));
|
||||
double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY);
|
||||
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
|
||||
followCircleActive = true;
|
||||
data.changeHealth(delta * GameData.HP_DRAIN_MULTIPLIER);
|
||||
|
@ -412,11 +411,11 @@ public class Slider implements HitObject {
|
|||
// held during new repeat
|
||||
if (isNewRepeat) {
|
||||
ticksHit++;
|
||||
if (currentRepeats % 2 > 0) // last circle
|
||||
if (currentRepeats % 2 > 0) { // last circle
|
||||
int lastIndex = hitObject.getSliderX().length;
|
||||
data.sliderTickResult(trackPosition, GameData.HIT_SLIDER30,
|
||||
hitObject.getSliderX()[lastIndex], hitObject.getSliderY()[lastIndex],
|
||||
hitObject, currentRepeats);
|
||||
else // first circle
|
||||
curve.getX(lastIndex), curve.getY(lastIndex), hitObject, currentRepeats);
|
||||
} else // first circle
|
||||
data.sliderTickResult(trackPosition, GameData.HIT_SLIDER30,
|
||||
c[0], c[1], hitObject, currentRepeats);
|
||||
}
|
||||
|
|
|
@ -194,7 +194,7 @@ public class Spinner implements HitObject {
|
|||
public boolean mousePressed(int x, int y) { return false; } // not used
|
||||
|
||||
@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();
|
||||
|
||||
// end of spinner
|
||||
|
@ -204,7 +204,7 @@ public class Spinner implements HitObject {
|
|||
}
|
||||
|
||||
// game button is released
|
||||
if (isSpinning && !(Utils.isGameKeyPressed() || GameMod.RELAX.isActive()))
|
||||
if (isSpinning && !(keyPressed || GameMod.RELAX.isActive()))
|
||||
isSpinning = false;
|
||||
|
||||
// spin automatically
|
||||
|
@ -224,7 +224,7 @@ public class Spinner implements HitObject {
|
|||
angle = (float) Math.atan2(mouseY - (height / 2), mouseX - (width / 2));
|
||||
|
||||
// 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;
|
||||
isSpinning = true;
|
||||
return false;
|
||||
|
|
|
@ -34,6 +34,12 @@ public abstract class Curve {
|
|||
/** The color of this curve. */
|
||||
protected Color color;
|
||||
|
||||
/** The scaled starting x, y coordinates. */
|
||||
protected float x, y;
|
||||
|
||||
/** The scaled slider x, y coordinate lists. */
|
||||
protected float[] sliderX, sliderY;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param hitObject the associated OsuHitObject
|
||||
|
@ -41,6 +47,10 @@ public abstract class Curve {
|
|||
*/
|
||||
protected Curve(OsuHitObject hitObject, Color color) {
|
||||
this.hitObject = hitObject;
|
||||
this.x = hitObject.getScaledX();
|
||||
this.y = hitObject.getScaledY();
|
||||
this.sliderX = hitObject.getScaledSliderX();
|
||||
this.sliderY = hitObject.getScaledSliderY();
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
|
@ -67,18 +77,16 @@ public abstract class Curve {
|
|||
public abstract float getStartAngle();
|
||||
|
||||
/**
|
||||
* Returns the x coordinate of the control point at index i.
|
||||
* Returns the scaled x coordinate of the control point at index i.
|
||||
* @param i the control point index
|
||||
*/
|
||||
protected float getX(int i) {
|
||||
return (i == 0) ? hitObject.getX() : hitObject.getSliderX()[i - 1];
|
||||
}
|
||||
public float getX(int i) { return (i == 0) ? x : sliderX[i - 1]; }
|
||||
|
||||
/**
|
||||
* Returns the y coordinate of the control point at index i.
|
||||
* Returns the scaled y coordinate of the control point at index i.
|
||||
* @param i the control point index
|
||||
*/
|
||||
protected float getY(int i) {
|
||||
return (i == 0) ? hitObject.getY() : hitObject.getSliderY()[i - 1];
|
||||
}
|
||||
public float getY(int i) { return (i == 0) ? y : sliderY[i - 1]; }
|
||||
|
||||
/**
|
||||
* Linear interpolation of a and b at t.
|
||||
|
|
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);
|
||||
}
|
||||
}
|
373
src/itdelatrisu/opsu/replay/Replay.java
Normal file
373
src/itdelatrisu/opsu/replay/Replay.java
Normal file
|
@ -0,0 +1,373 @@
|
|||
/*
|
||||
* 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.ErrorHandler;
|
||||
import itdelatrisu.opsu.Options;
|
||||
import itdelatrisu.opsu.OsuReader;
|
||||
import itdelatrisu.opsu.OsuWriter;
|
||||
import itdelatrisu.opsu.Utils;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.CharsetEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import lzma.streams.LzmaOutputStream;
|
||||
|
||||
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;
|
||||
|
||||
/** Whether or not the replay data has been loaded from the file. */
|
||||
public boolean loaded = false;
|
||||
|
||||
/** 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 int 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;
|
||||
|
||||
/** Seed. (?) */
|
||||
public int seed;
|
||||
|
||||
/** Seed string. */
|
||||
private static final String SEED_STRING = "-12345";
|
||||
|
||||
/**
|
||||
* Empty constructor.
|
||||
*/
|
||||
public Replay() {}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param file the file to load from
|
||||
*/
|
||||
public Replay(File file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the replay data.
|
||||
* @throws IOException failure to load the data
|
||||
*/
|
||||
public void load() throws IOException {
|
||||
if (loaded)
|
||||
return;
|
||||
|
||||
OsuReader reader = new OsuReader(file);
|
||||
loadHeader(reader);
|
||||
loadData(reader);
|
||||
reader.close();
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 e) {
|
||||
Log.warn(String.format("Failed to load 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(",");
|
||||
lzma.close();
|
||||
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 {
|
||||
if (tokens[0].equals(SEED_STRING)) {
|
||||
seed = Integer.parseInt(tokens[3]);
|
||||
continue;
|
||||
}
|
||||
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 e) {
|
||||
Log.warn(String.format("Failed to parse frame: '%s'", frame), e);
|
||||
}
|
||||
}
|
||||
this.frames = replayFrameList.toArray(new ReplayFrame[replayFrameList.size()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the replay data to a file in the replays directory.
|
||||
*/
|
||||
public void save() {
|
||||
// create replay directory
|
||||
File dir = Options.getReplayDir();
|
||||
if (!dir.isDirectory()) {
|
||||
if (!dir.mkdir()) {
|
||||
ErrorHandler.error("Failed to create replay directory.", null, false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// write file in new thread
|
||||
final File file = new File(dir, String.format("%s.osr", getReplayFilename()));
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try (FileOutputStream out = new FileOutputStream(file)) {
|
||||
OsuWriter writer = new OsuWriter(out);
|
||||
|
||||
// header
|
||||
writer.write(mode);
|
||||
writer.write(version);
|
||||
writer.write(beatmapHash);
|
||||
writer.write(playerName);
|
||||
writer.write(replayHash);
|
||||
writer.write(hit300);
|
||||
writer.write(hit100);
|
||||
writer.write(hit50);
|
||||
writer.write(geki);
|
||||
writer.write(katu);
|
||||
writer.write(miss);
|
||||
writer.write(score);
|
||||
writer.write(combo);
|
||||
writer.write(perfect);
|
||||
writer.write(mods);
|
||||
|
||||
// life data
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (lifeFrames != null) {
|
||||
NumberFormat nf = new DecimalFormat("##.##");
|
||||
for (int i = 0; i < lifeFrames.length; i++) {
|
||||
LifeFrame frame = lifeFrames[i];
|
||||
sb.append(String.format("%d|%s,",
|
||||
frame.getTime(), nf.format(frame.getPercentage())));
|
||||
}
|
||||
}
|
||||
writer.write(sb.toString());
|
||||
|
||||
// timestamp
|
||||
writer.write(timestamp);
|
||||
|
||||
// LZMA-encoded replay data
|
||||
if (frames != null && frames.length > 0) {
|
||||
// build full frame string
|
||||
NumberFormat nf = new DecimalFormat("###.#####");
|
||||
sb = new StringBuilder();
|
||||
for (int i = 0; i < frames.length; i++) {
|
||||
ReplayFrame frame = frames[i];
|
||||
sb.append(String.format("%d|%s|%s|%d,",
|
||||
frame.getTimeDiff(), nf.format(frame.getX()),
|
||||
nf.format(frame.getY()), frame.getKeys()));
|
||||
}
|
||||
sb.append(String.format("%s|0|0|%d", SEED_STRING, seed));
|
||||
|
||||
// get bytes from string
|
||||
CharsetEncoder encoder = StandardCharsets.US_ASCII.newEncoder();
|
||||
CharBuffer buffer = CharBuffer.wrap(sb);
|
||||
byte[] bytes = encoder.encode(buffer).array();
|
||||
|
||||
// compress data
|
||||
ByteArrayOutputStream bout = new ByteArrayOutputStream();
|
||||
LzmaOutputStream compressedOut = new LzmaOutputStream.Builder(bout).useMediumDictionarySize().build();
|
||||
try {
|
||||
compressedOut.write(bytes);
|
||||
} catch (IOException e) {
|
||||
// possible OOM: https://github.com/jponge/lzma-java/issues/9
|
||||
ErrorHandler.error("LZMA compression failed (possible out-of-memory error).", e, true);
|
||||
}
|
||||
compressedOut.close();
|
||||
bout.close();
|
||||
|
||||
// write to file
|
||||
byte[] compressed = bout.toByteArray();
|
||||
writer.write(compressed.length);
|
||||
writer.write(compressed);
|
||||
} else
|
||||
writer.write(0);
|
||||
|
||||
writer.close();
|
||||
} catch (IOException e) {
|
||||
ErrorHandler.error("Could not save replay data.", e, true);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file name of where the replay should be saved and loaded,
|
||||
* or null if the required fields are not set.
|
||||
*/
|
||||
public String getReplayFilename() {
|
||||
if (replayHash == null)
|
||||
return null;
|
||||
|
||||
return String.format("%s-%d%d%d%d%d%d",
|
||||
replayHash, hit300, hit100, hit50, geki, katu, miss);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final int LINE_SPLIT = 5;
|
||||
final int MAX_LINES = 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 ("); sb.append(lifeFrames.length); sb.append(" total):\n");
|
||||
for (int i = 0; i < lifeFrames.length && i < MAX_LINES; 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 ("); sb.append(frames.length); sb.append(" total):\n");
|
||||
for (int i = 0; i < frames.length && i < MAX_LINES; i++) {
|
||||
if (i % LINE_SPLIT == 0)
|
||||
sb.append('\t');
|
||||
sb.append(frames[i]);
|
||||
sb.append((i % LINE_SPLIT == LINE_SPLIT - 1) ? '\n' : ' ');
|
||||
}
|
||||
sb.append('\n');
|
||||
}
|
||||
sb.append("Seed: "); sb.append(seed); sb.append('\n');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
122
src/itdelatrisu/opsu/replay/ReplayFrame.java
Normal file
122
src/itdelatrisu/opsu/replay/ReplayFrame.java
Normal file
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Returns the start frame.
|
||||
* @param t the value for the {@code time} and {@code timeDiff} fields
|
||||
*/
|
||||
public static ReplayFrame getStartFrame(int t) {
|
||||
return new ReplayFrame(t, t, 256, -500, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param timeDiff time since the previous action (in ms)
|
||||
* @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; }
|
||||
|
||||
/**
|
||||
* Sets the time since the previous action, in milliseconds.
|
||||
*/
|
||||
public void setTimeDiff(int diff) { this.timeDiff = diff; }
|
||||
|
||||
/**
|
||||
* Returns the raw cursor x coordinate.
|
||||
*/
|
||||
public float getX() { return x; }
|
||||
|
||||
/**
|
||||
* Returns the raw cursor y coordinate.
|
||||
*/
|
||||
public float getY() { return y; }
|
||||
|
||||
/**
|
||||
* Returns the scaled cursor x coordinate.
|
||||
*/
|
||||
public int getScaledX() { return (int) (x * OsuHitObject.getXMultiplier() + OsuHitObject.getXOffset()); }
|
||||
|
||||
/**
|
||||
* Returns the scaled cursor y coordinate.
|
||||
*/
|
||||
public int getScaledY() { 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);
|
||||
}
|
||||
}
|
|
@ -27,6 +27,7 @@ import itdelatrisu.opsu.Opsu;
|
|||
import itdelatrisu.opsu.Options;
|
||||
import itdelatrisu.opsu.OsuFile;
|
||||
import itdelatrisu.opsu.OsuHitObject;
|
||||
import itdelatrisu.opsu.OsuParser;
|
||||
import itdelatrisu.opsu.OsuTimingPoint;
|
||||
import itdelatrisu.opsu.ScoreData;
|
||||
import itdelatrisu.opsu.UI;
|
||||
|
@ -35,17 +36,22 @@ import itdelatrisu.opsu.audio.HitSound;
|
|||
import itdelatrisu.opsu.audio.MusicController;
|
||||
import itdelatrisu.opsu.audio.SoundController;
|
||||
import itdelatrisu.opsu.audio.SoundEffect;
|
||||
import itdelatrisu.opsu.db.OsuDB;
|
||||
import itdelatrisu.opsu.db.ScoreDB;
|
||||
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.LinkedList;
|
||||
import java.util.Stack;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.lwjgl.input.Keyboard;
|
||||
import org.lwjgl.opengl.Display;
|
||||
import org.newdawn.slick.Animation;
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.GameContainer;
|
||||
|
@ -71,6 +77,8 @@ public class Game extends BasicGameState {
|
|||
NEW,
|
||||
/** Manual retry. */
|
||||
MANUAL,
|
||||
/** Replay. */
|
||||
REPLAY,
|
||||
/** Health is zero: no-continue/force restart. */
|
||||
LOSE;
|
||||
}
|
||||
|
@ -146,6 +154,42 @@ public class Game extends BasicGameState {
|
|||
/** Number of retries. */
|
||||
private int retries = 0;
|
||||
|
||||
/** Whether or not this game is a replay. */
|
||||
private boolean isReplay = false;
|
||||
|
||||
/** 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 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. */
|
||||
private int lastReplayTime = 0;
|
||||
|
||||
/** The last game keys pressed. */
|
||||
private int lastKeysPressed = ReplayFrame.KEY_NONE;
|
||||
|
||||
/** The previous game mod state (before the replay). */
|
||||
private int previousMods = 0;
|
||||
|
||||
/** The list of current replay frames (for recording replays). */
|
||||
private LinkedList<ReplayFrame> replayFrames;
|
||||
|
||||
// game-related variables
|
||||
private GameContainer container;
|
||||
private StateBasedGame game;
|
||||
|
@ -259,7 +303,10 @@ public class Game extends BasicGameState {
|
|||
|
||||
if (GameMod.AUTO.isActive())
|
||||
GameImage.UNRANKED.getImage().drawCentered(width / 2, height * 0.077f);
|
||||
UI.draw(g);
|
||||
if (!isReplay)
|
||||
UI.draw(g);
|
||||
else
|
||||
UI.draw(g, replayX, replayY, replayKeyPressed);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -365,14 +412,24 @@ public class Game extends BasicGameState {
|
|||
cursorCirclePulse.drawCentered(pausedMouseX, pausedMouseY);
|
||||
}
|
||||
|
||||
UI.draw(g);
|
||||
if (!isReplay)
|
||||
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 (!isReplay) {
|
||||
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,15 +480,31 @@ 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
|
||||
// if checkpoint used, skip ranking screen
|
||||
if (checkpointLoaded)
|
||||
game.closeRequested();
|
||||
else { // go to ranking screen
|
||||
|
||||
// go to ranking screen
|
||||
else {
|
||||
((GameRanking) game.getState(Opsu.STATE_GAMERANKING)).setGameData(data);
|
||||
if (!isReplay && replayFrames != null) {
|
||||
// finalize replay frames with start/skip frames
|
||||
if (!replayFrames.isEmpty())
|
||||
replayFrames.getFirst().setTimeDiff(replaySkipTime * -1);
|
||||
replayFrames.addFirst(ReplayFrame.getStartFrame(replaySkipTime));
|
||||
replayFrames.addFirst(ReplayFrame.getStartFrame(0));
|
||||
Replay r = data.getReplay(replayFrames.toArray(new ReplayFrame[replayFrames.size()]));
|
||||
if (r != null)
|
||||
r.save();
|
||||
}
|
||||
ScoreData score = data.getScoreData(osu);
|
||||
if (!GameMod.AUTO.isActive() && !GameMod.RELAX.isActive() && !GameMod.AUTOPILOT.isActive())
|
||||
|
||||
// add score to database
|
||||
if (!GameMod.AUTO.isActive() && !GameMod.RELAX.isActive() && !GameMod.AUTOPILOT.isActive() && !isReplay)
|
||||
ScoreDB.addScore(score);
|
||||
|
||||
game.enterState(Opsu.STATE_GAMERANKING, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||
}
|
||||
return;
|
||||
|
@ -453,6 +526,15 @@ public class Game extends BasicGameState {
|
|||
}
|
||||
}
|
||||
|
||||
// replays
|
||||
if (isReplay) {
|
||||
// skip intro
|
||||
if (replaySkipTime > 0 && trackPosition > replaySkipTime) {
|
||||
skipIntro();
|
||||
replaySkipTime = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// song beginning
|
||||
if (objectIndex == 0 && trackPosition < osu.objects[0].getTime())
|
||||
return; // nothing to do here
|
||||
|
@ -478,7 +560,7 @@ public class Game extends BasicGameState {
|
|||
}
|
||||
|
||||
// pause game if focus lost
|
||||
if (!container.hasFocus() && !GameMod.AUTO.isActive()) {
|
||||
if (!container.hasFocus() && !GameMod.AUTO.isActive() && !isReplay) {
|
||||
if (pauseTime < 0) {
|
||||
pausedMouseX = mouseX;
|
||||
pausedMouseY = mouseY;
|
||||
|
@ -503,18 +585,21 @@ public class Game extends BasicGameState {
|
|||
}
|
||||
|
||||
// game over, force a restart
|
||||
restart = Restart.LOSE;
|
||||
game.enterState(Opsu.STATE_GAMEPAUSEMENU);
|
||||
if (!isReplay) {
|
||||
restart = Restart.LOSE;
|
||||
game.enterState(Opsu.STATE_GAMEPAUSEMENU);
|
||||
}
|
||||
}
|
||||
|
||||
// update objects (loop in unlikely event of any skipped indexes)
|
||||
boolean keyPressed = ((isReplay && 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,17 +614,17 @@ public class Game extends BasicGameState {
|
|||
int trackPosition = MusicController.getPosition();
|
||||
|
||||
// game keys
|
||||
if (!Keyboard.isRepeatEvent()) {
|
||||
if (!Keyboard.isRepeatEvent() && !isReplay) {
|
||||
if (key == Options.getGameKeyLeft())
|
||||
gameKeyPressed(Input.MOUSE_LEFT_BUTTON, input.getMouseX(), input.getMouseY());
|
||||
gameKeyPressed(ReplayFrame.KEY_K1, input.getMouseX(), input.getMouseY());
|
||||
else if (key == Options.getGameKeyRight())
|
||||
gameKeyPressed(Input.MOUSE_RIGHT_BUTTON, input.getMouseX(), input.getMouseY());
|
||||
gameKeyPressed(ReplayFrame.KEY_K2, input.getMouseX(), input.getMouseY());
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
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() || isReplay) {
|
||||
game.closeRequested();
|
||||
break;
|
||||
}
|
||||
|
@ -592,6 +677,7 @@ public class Game extends BasicGameState {
|
|||
if (checkpoint == 0 || checkpoint > osu.endTime)
|
||||
break; // invalid checkpoint
|
||||
try {
|
||||
killReplayThread();
|
||||
restart = Restart.MANUAL;
|
||||
enter(container, game);
|
||||
checkpointLoaded = true;
|
||||
|
@ -636,6 +722,14 @@ public class Game extends BasicGameState {
|
|||
if (Options.isMouseDisabled())
|
||||
return;
|
||||
|
||||
// watching replay
|
||||
if (isReplay) {
|
||||
// only allow skip button
|
||||
if (button != Input.MOUSE_MIDDLE_BUTTON && skipButton.contains(x, y))
|
||||
skipIntro();
|
||||
return;
|
||||
}
|
||||
|
||||
// mouse wheel: pause the game
|
||||
if (button == Input.MOUSE_MIDDLE_BUTTON && !Options.isMouseWheelDisabled()) {
|
||||
int trackPosition = MusicController.getPosition();
|
||||
|
@ -650,16 +744,16 @@ public class Game extends BasicGameState {
|
|||
return;
|
||||
}
|
||||
|
||||
gameKeyPressed(button, x, y);
|
||||
gameKeyPressed((button == Input.MOUSE_LEFT_BUTTON) ? ReplayFrame.KEY_M1 : ReplayFrame.KEY_M2, x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a game key pressed event.
|
||||
* @param button the index of the button pressed
|
||||
* @param keys the game keys pressed
|
||||
* @param x the mouse x coordinate
|
||||
* @param y the mouse y coordinate
|
||||
*/
|
||||
private void gameKeyPressed(int button, int x, int y) {
|
||||
private void gameKeyPressed(int keys, int x, int y) {
|
||||
// returning from pause screen
|
||||
if (pauseTime > -1) {
|
||||
double distance = Math.hypot(pausedMouseX - x, pausedMouseY - y);
|
||||
|
@ -690,6 +784,8 @@ public class Game extends BasicGameState {
|
|||
if (GameMod.AUTO.isActive() || GameMod.RELAX.isActive())
|
||||
return;
|
||||
|
||||
addReplayFrame(x, y, lastKeysPressed | keys);
|
||||
|
||||
// circles
|
||||
if (hitObject.isCircle() && hitObjects[objectIndex].mousePressed(x, y))
|
||||
objectIndex++; // circle hit
|
||||
|
@ -699,6 +795,36 @@ public class Game extends BasicGameState {
|
|||
hitObjects[objectIndex].mousePressed(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(int button, int x, int y) {
|
||||
if (Options.isMouseDisabled())
|
||||
return;
|
||||
|
||||
if (button == Input.MOUSE_MIDDLE_BUTTON)
|
||||
return;
|
||||
|
||||
int key = (button == Input.MOUSE_LEFT_BUTTON) ? ReplayFrame.KEY_M1 : ReplayFrame.KEY_M2;
|
||||
if ((lastKeysPressed & key) > 0)
|
||||
addReplayFrame(x, y, ReplayFrame.KEY_NONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyReleased(int key, char c) {
|
||||
if ((key == Options.getGameKeyLeft() && (lastKeysPressed & ReplayFrame.KEY_K1) > 0) ||
|
||||
(key == Options.getGameKeyRight() && (lastKeysPressed & ReplayFrame.KEY_K2) > 0))
|
||||
addReplayFrame(input.getMouseX(), input.getMouseY(), ReplayFrame.KEY_NONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseMoved(int oldx, int oldy, int newx, int newy) {
|
||||
addReplayFrame(newx, newy, lastKeysPressed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseDragged(int oldx, int oldy, int newx, int newy) {
|
||||
addReplayFrame(newx, newy, lastKeysPressed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseWheelMoved(int newValue) {
|
||||
if (Options.isMouseWheelDisabled() || Options.isMouseDisabled())
|
||||
|
@ -711,8 +837,6 @@ public class Game extends BasicGameState {
|
|||
public void enter(GameContainer container, StateBasedGame game)
|
||||
throws SlickException {
|
||||
UI.enter();
|
||||
if (restart == Restart.NEW)
|
||||
osu = MusicController.getOsuFile();
|
||||
|
||||
if (osu == null || osu.objects == null)
|
||||
throw new RuntimeException("Running game with no OsuFile loaded.");
|
||||
|
@ -727,10 +851,11 @@ public class Game extends BasicGameState {
|
|||
loadImages();
|
||||
setMapModifiers();
|
||||
retries = 0;
|
||||
} else {
|
||||
} else if (restart == Restart.MANUAL) {
|
||||
// retry
|
||||
retries++;
|
||||
}
|
||||
} else if (restart == Restart.REPLAY)
|
||||
retries = 0;
|
||||
|
||||
// reset game data
|
||||
resetGameData();
|
||||
|
@ -769,6 +894,75 @@ public class Game extends BasicGameState {
|
|||
}
|
||||
}
|
||||
|
||||
// load replay frames
|
||||
if (isReplay) {
|
||||
// unhide cursor
|
||||
UI.showCursor();
|
||||
|
||||
// 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.getScaledX();
|
||||
replayY = frame.getScaledY();
|
||||
replayKeyPressed = frame.isKeyPressed();
|
||||
} else
|
||||
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();
|
||||
if (replayKeyPressed) // send a key press
|
||||
gameKeyPressed(frame.getKeys(), replayX, replayY);
|
||||
replayIndex++;
|
||||
}
|
||||
|
||||
// out of frames
|
||||
if (replayIndex >= replay.frames.length)
|
||||
break;
|
||||
|
||||
// sleep execution
|
||||
try {
|
||||
Thread.sleep(0, 256000);
|
||||
} catch (InterruptedException e) {}
|
||||
}
|
||||
}
|
||||
};
|
||||
replayThreadRunning = true;
|
||||
replayThread.start();
|
||||
}
|
||||
|
||||
// initialize replay-recording structures
|
||||
else {
|
||||
lastReplayTime = 0;
|
||||
lastKeysPressed = ReplayFrame.KEY_NONE;
|
||||
replaySkipTime = -1;
|
||||
replayFrames = new LinkedList<ReplayFrame>();
|
||||
replayFrames.add(new ReplayFrame(0, 0, input.getMouseX(), input.getMouseY(), 0));
|
||||
}
|
||||
|
||||
leadInTime = osu.audioLeadIn + approachTime;
|
||||
restart = Restart.FALSE;
|
||||
}
|
||||
|
@ -776,11 +970,31 @@ 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);
|
||||
// }
|
||||
|
||||
// replays
|
||||
if (isReplay) {
|
||||
GameMod.loadModState(previousMods);
|
||||
UI.hideCursor();
|
||||
killReplayThread();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all required data from an OsuFile.
|
||||
* @param osu the OsuFile to load
|
||||
*/
|
||||
public void loadOsuFile(OsuFile osu) {
|
||||
this.osu = osu;
|
||||
Display.setTitle(String.format("%s - %s", game.getTitle(), osu.toString()));
|
||||
if (osu.timingPoints == null || osu.combo == null)
|
||||
OsuDB.load(osu, OsuDB.LOAD_ARRAY);
|
||||
OsuParser.parseHitObjects(osu);
|
||||
HitSound.setDefaultSampleSet(osu.sampleSet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all game data and structures.
|
||||
|
@ -805,6 +1019,7 @@ public class Game extends BasicGameState {
|
|||
checkpointLoaded = false;
|
||||
deaths = 0;
|
||||
deathTime = -1;
|
||||
replayFrames = null;
|
||||
|
||||
System.gc();
|
||||
}
|
||||
|
@ -813,15 +1028,17 @@ public class Game extends BasicGameState {
|
|||
* Skips the beginning of a track.
|
||||
* @return true if skipped, false otherwise
|
||||
*/
|
||||
private boolean skipIntro() {
|
||||
private synchronized 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 = (isReplay) ? -1 : trackPosition;
|
||||
if (replayThread != null && replayThread.isAlive())
|
||||
replayThread.interrupt();
|
||||
MusicController.setPosition(firstObjectTime - SKIP_OFFSET);
|
||||
SoundController.playSound(SoundEffect.MENUHIT);
|
||||
return true;
|
||||
|
@ -950,4 +1167,52 @@ public class Game extends BasicGameState {
|
|||
* Returns the slider multiplier given by the current timing point.
|
||||
*/
|
||||
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.
|
||||
* @param replay the replay
|
||||
*/
|
||||
public void setReplay(Replay replay) {
|
||||
if (replay == null) {
|
||||
this.isReplay = false;
|
||||
this.replay = null;
|
||||
} else {
|
||||
if (replay.frames == null) {
|
||||
ErrorHandler.error("Attempting to set a replay with no frames.", null, false);
|
||||
return;
|
||||
}
|
||||
this.isReplay = true;
|
||||
this.replay = replay;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a replay frame to the list.
|
||||
* @param x the cursor x coordinate
|
||||
* @param y the cursor y coordinate
|
||||
* @param keys the keys pressed
|
||||
*/
|
||||
private void addReplayFrame(int x, int y, int keys) {
|
||||
if (isReplay)
|
||||
return;
|
||||
|
||||
int time = MusicController.getPosition();
|
||||
int timeDiff = time - lastReplayTime;
|
||||
lastReplayTime = time;
|
||||
lastKeysPressed = keys;
|
||||
int cx = (int) ((x - OsuHitObject.getXOffset()) / OsuHitObject.getXMultiplier());
|
||||
int cy = (int) ((y - OsuHitObject.getYOffset()) / OsuHitObject.getYMultiplier());
|
||||
replayFrames.add(new ReplayFrame(timeDiff, time, cx, cy, lastKeysPressed));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,10 @@ import itdelatrisu.opsu.Utils;
|
|||
import itdelatrisu.opsu.audio.MusicController;
|
||||
import itdelatrisu.opsu.audio.SoundController;
|
||||
import itdelatrisu.opsu.audio.SoundEffect;
|
||||
import itdelatrisu.opsu.replay.Replay;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.lwjgl.opengl.Display;
|
||||
import org.newdawn.slick.Color;
|
||||
|
@ -41,12 +45,13 @@ import org.newdawn.slick.state.BasicGameState;
|
|||
import org.newdawn.slick.state.StateBasedGame;
|
||||
import org.newdawn.slick.state.transition.FadeInTransition;
|
||||
import org.newdawn.slick.state.transition.FadeOutTransition;
|
||||
import org.newdawn.slick.util.Log;
|
||||
|
||||
/**
|
||||
* "Game Ranking" (score card) state.
|
||||
* <ul>
|
||||
* <li>[Retry] - restart game (return to game state)
|
||||
* <li>[Exit] - return to main menu state
|
||||
* <li>[Replay] - watch replay (return to game state)
|
||||
* <li>[Back] - return to song menu state
|
||||
* </ul>
|
||||
*/
|
||||
|
@ -54,8 +59,11 @@ public class GameRanking extends BasicGameState {
|
|||
/** Associated GameData object. */
|
||||
private GameData data;
|
||||
|
||||
/** "Retry" and "Exit" buttons. */
|
||||
private MenuButton retryButton, exitButton;
|
||||
/** "Retry" and "Replay" buttons. */
|
||||
private MenuButton retryButton, replayButton;
|
||||
|
||||
/** Button coordinates. */
|
||||
private float retryY, replayY;
|
||||
|
||||
// game-related variables
|
||||
private GameContainer container;
|
||||
|
@ -78,18 +86,14 @@ public class GameRanking extends BasicGameState {
|
|||
int height = container.getHeight();
|
||||
|
||||
// buttons
|
||||
Image retry = GameImage.RANKING_RETRY.getImage();
|
||||
Image exit = GameImage.RANKING_EXIT.getImage();
|
||||
retryButton = new MenuButton(retry,
|
||||
width - (retry.getWidth() / 2f),
|
||||
(height * 0.97f) - (exit.getHeight() * 1.5f)
|
||||
);
|
||||
exitButton = new MenuButton(exit,
|
||||
width - (exit.getWidth() / 2f),
|
||||
(height * 0.97f) - (exit.getHeight() / 2f)
|
||||
);
|
||||
retryButton.setHoverFade(0.6f);
|
||||
exitButton.setHoverFade(0.6f);
|
||||
Image retry = GameImage.PAUSE_RETRY.getImage();
|
||||
Image replay = GameImage.PAUSE_REPLAY.getImage();
|
||||
replayY = (height * 0.985f) - replay.getHeight() / 2f;
|
||||
retryY = replayY - (replay.getHeight() / 2f) - (retry.getHeight() / 1.975f);
|
||||
retryButton = new MenuButton(retry, width - (retry.getWidth() / 2f), retryY);
|
||||
replayButton = new MenuButton(replay, width - (replay.getWidth() / 2f), replayY);
|
||||
retryButton.setHoverFade();
|
||||
replayButton.setHoverFade();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -108,10 +112,9 @@ public class GameRanking extends BasicGameState {
|
|||
data.drawRankingElements(g, osu);
|
||||
|
||||
// buttons
|
||||
if (data.isGameplay()) {
|
||||
replayButton.draw();
|
||||
if (data.isGameplay())
|
||||
retryButton.draw();
|
||||
exitButton.draw();
|
||||
}
|
||||
UI.getBackButton().draw();
|
||||
|
||||
UI.draw(g);
|
||||
|
@ -122,10 +125,10 @@ public class GameRanking extends BasicGameState {
|
|||
throws SlickException {
|
||||
UI.update(delta);
|
||||
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
|
||||
if (data.isGameplay()) {
|
||||
replayButton.hoverUpdate(delta, mouseX, mouseY);
|
||||
if (data.isGameplay())
|
||||
retryButton.hoverUpdate(delta, mouseX, mouseY);
|
||||
exitButton.hoverUpdate(delta, mouseX, mouseY);
|
||||
} else
|
||||
else
|
||||
MusicController.loopTrackIfEnded(true);
|
||||
UI.getBackButton().hoverUpdate(delta, mouseX, mouseY);
|
||||
}
|
||||
|
@ -157,27 +160,47 @@ public class GameRanking extends BasicGameState {
|
|||
if (button == Input.MOUSE_MIDDLE_BUTTON)
|
||||
return;
|
||||
|
||||
if (data.isGameplay()) {
|
||||
if (retryButton.contains(x, y)) {
|
||||
OsuFile osu = MusicController.getOsuFile();
|
||||
Display.setTitle(String.format("%s - %s", game.getTitle(), osu.toString()));
|
||||
((Game) game.getState(Opsu.STATE_GAME)).setRestart(Game.Restart.MANUAL);
|
||||
SoundController.playSound(SoundEffect.MENUHIT);
|
||||
game.enterState(Opsu.STATE_GAME, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||
return;
|
||||
} else if (exitButton.contains(x, y)) {
|
||||
SoundController.playSound(SoundEffect.MENUBACK);
|
||||
((MainMenu) game.getState(Opsu.STATE_MAINMENU)).reset();
|
||||
((SongMenu) game.getState(Opsu.STATE_SONGMENU)).resetGameDataOnLoad();
|
||||
UI.resetCursor();
|
||||
game.enterState(Opsu.STATE_MAINMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// back to menu
|
||||
if (UI.getBackButton().contains(x, y)) {
|
||||
returnToSongMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
// replay
|
||||
Game gameState = (Game) game.getState(Opsu.STATE_GAME);
|
||||
boolean returnToGame = false;
|
||||
if (replayButton.contains(x, y)) {
|
||||
Replay r = data.getReplay(null);
|
||||
if (r != null) {
|
||||
try {
|
||||
r.load();
|
||||
gameState.setReplay(r);
|
||||
gameState.setRestart((data.isGameplay()) ? Game.Restart.REPLAY : Game.Restart.NEW);
|
||||
returnToGame = true;
|
||||
} catch (FileNotFoundException e) {
|
||||
UI.sendBarNotification("Replay file not found.");
|
||||
} catch (IOException e) {
|
||||
Log.error("Failed to load replay data.", e);
|
||||
UI.sendBarNotification("Failed to load replay data. See log for details.");
|
||||
}
|
||||
} else
|
||||
UI.sendBarNotification("Replay file not found.");
|
||||
}
|
||||
|
||||
// retry
|
||||
else if (data.isGameplay() && retryButton.contains(x, y)) {
|
||||
gameState.setReplay(null);
|
||||
gameState.setRestart(Game.Restart.MANUAL);
|
||||
returnToGame = true;
|
||||
}
|
||||
|
||||
if (returnToGame) {
|
||||
OsuFile osu = MusicController.getOsuFile();
|
||||
gameState.loadOsuFile(osu);
|
||||
SoundController.playSound(SoundEffect.MENUHIT);
|
||||
game.enterState(Opsu.STATE_GAME, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -188,17 +211,21 @@ public class GameRanking extends BasicGameState {
|
|||
if (!data.isGameplay()) {
|
||||
if (!MusicController.isTrackDimmed())
|
||||
MusicController.toggleTrackDimmed(0.5f);
|
||||
replayButton.setY(retryY);
|
||||
} else {
|
||||
SoundController.playSound(SoundEffect.APPLAUSE);
|
||||
retryButton.resetHover();
|
||||
exitButton.resetHover();
|
||||
replayButton.setY(replayY);
|
||||
}
|
||||
replayButton.resetHover();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void leave(GameContainer container, StateBasedGame game)
|
||||
throws SlickException {
|
||||
this.data = null;
|
||||
if (MusicController.isTrackDimmed())
|
||||
MusicController.toggleTrackDimmed(1f);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -97,8 +97,8 @@ public class MainMenu extends BasicGameState {
|
|||
/** Background alpha level (for fade-in effect). */
|
||||
private float bgAlpha = 0f;
|
||||
|
||||
/** Whether or not an update notification was already sent. */
|
||||
private boolean updateNotification = false;
|
||||
/** Whether or not a notification was already sent upon entering. */
|
||||
private boolean enterNotification = false;
|
||||
|
||||
/** Music position bar coordinates and dimensions. */
|
||||
private float musicBarX, musicBarY, musicBarWidth, musicBarHeight;
|
||||
|
@ -388,9 +388,14 @@ public class MainMenu extends BasicGameState {
|
|||
public void enter(GameContainer container, StateBasedGame game)
|
||||
throws SlickException {
|
||||
UI.enter();
|
||||
if (!updateNotification && Updater.get().getStatus() == Updater.Status.UPDATE_AVAILABLE) {
|
||||
UI.sendBarNotification("An opsu! update is available.");
|
||||
updateNotification = true;
|
||||
if (!enterNotification) {
|
||||
if (Updater.get().getStatus() == Updater.Status.UPDATE_AVAILABLE) {
|
||||
UI.sendBarNotification("An opsu! update is available.");
|
||||
enterNotification = true;
|
||||
} else if (Updater.get().justUpdated()) {
|
||||
UI.sendBarNotification("opsu! is now up to date!");
|
||||
enterNotification = true;
|
||||
}
|
||||
}
|
||||
|
||||
// reset button hover states if mouse is not currently hovering over the button
|
||||
|
|
|
@ -34,7 +34,6 @@ import itdelatrisu.opsu.ScoreData;
|
|||
import itdelatrisu.opsu.SongSort;
|
||||
import itdelatrisu.opsu.UI;
|
||||
import itdelatrisu.opsu.Utils;
|
||||
import itdelatrisu.opsu.audio.HitSound;
|
||||
import itdelatrisu.opsu.audio.MultiClip;
|
||||
import itdelatrisu.opsu.audio.MusicController;
|
||||
import itdelatrisu.opsu.audio.SoundController;
|
||||
|
@ -1288,17 +1287,12 @@ public class SongMenu extends BasicGameState {
|
|||
return;
|
||||
|
||||
SoundController.playSound(SoundEffect.MENUHIT);
|
||||
OsuFile osu = MusicController.getOsuFile();
|
||||
Display.setTitle(String.format("%s - %s", game.getTitle(), osu.toString()));
|
||||
|
||||
// load any missing data
|
||||
if (osu.timingPoints == null || osu.combo == null)
|
||||
OsuDB.load(osu, OsuDB.LOAD_ARRAY);
|
||||
OsuParser.parseHitObjects(osu);
|
||||
HitSound.setDefaultSampleSet(osu.sampleSet);
|
||||
|
||||
MultiClip.destroyExtraClips();
|
||||
((Game) game.getState(Opsu.STATE_GAME)).setRestart(Game.Restart.NEW);
|
||||
OsuFile osu = MusicController.getOsuFile();
|
||||
Game gameState = (Game) game.getState(Opsu.STATE_GAME);
|
||||
gameState.loadOsuFile(osu);
|
||||
gameState.setRestart(Game.Restart.NEW);
|
||||
gameState.setReplay(null);
|
||||
game.enterState(Opsu.STATE_GAME, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
Loading…
Reference in New Issue
Block a user