Initial replay writing support.
- Added Replay.save() method to save replays to a file. - Separated Replay loading from the constructor into a load() method. - Added OsuWriter class to write replays. - Parse replay seeds (what do they do?). - Added Updater.getBuildDate() method to retrieve the current build date (for the replay 'version' field). - Added osu! mode constants in OsuFile. - Added methods to retrieve raw ReplayFrame coordinates. - Added replay fields/methods to GameData and Game state. - Added jponge/lzma-java dependency for LZMA compression, since it isn't implemented in Apache commons-compress... Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
This commit is contained in:
parent
3da7b320f0
commit
7536056a59
5
pom.xml
5
pom.xml
|
@ -202,5 +202,10 @@
|
|||
<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>
|
||||
|
|
|
@ -22,10 +22,13 @@ 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.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.concurrent.LinkedBlockingDeque;
|
||||
|
||||
import org.newdawn.slick.Animation;
|
||||
|
@ -275,6 +278,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;
|
||||
|
||||
|
@ -319,6 +325,7 @@ public class GameData {
|
|||
hitResultCount[HIT_300K] = 0;
|
||||
hitResultCount[HIT_100K] = s.katu;
|
||||
hitResultCount[HIT_MISS] = s.miss;
|
||||
this.replay = s.replay;
|
||||
|
||||
loadImages();
|
||||
}
|
||||
|
@ -342,6 +349,7 @@ public class GameData {
|
|||
comboEnd = 0;
|
||||
comboBurstIndex = -1;
|
||||
scoreData = null;
|
||||
replay = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1190,9 +1198,10 @@ public class GameData {
|
|||
* If score data already exists, the existing object will be returned
|
||||
* (i.e. this will not overwrite existing data).
|
||||
* @param osu the OsuFile
|
||||
* @param frames the replay frames
|
||||
* @return the ScoreData object
|
||||
*/
|
||||
public ScoreData getScoreData(OsuFile osu) {
|
||||
public ScoreData getScoreData(OsuFile osu, ReplayFrame[] frames) {
|
||||
if (scoreData != null)
|
||||
return scoreData;
|
||||
|
||||
|
@ -1214,9 +1223,44 @@ public class GameData {
|
|||
scoreData.combo = comboMax;
|
||||
scoreData.perfect = (comboMax == fullObjectCount);
|
||||
scoreData.mods = GameMod.getModState();
|
||||
scoreData.replay = getReplay(frames);
|
||||
return scoreData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Replay object encapsulating all game data.
|
||||
* If a replay already exists, the existing object will be returned
|
||||
* (i.e. this will not overwrite existing data).
|
||||
* @param frames the replay frames
|
||||
* @return the Replay object
|
||||
*/
|
||||
public Replay getReplay(ReplayFrame[] frames) {
|
||||
if (replay != null)
|
||||
return replay;
|
||||
|
||||
replay = new Replay();
|
||||
replay.mode = OsuFile.MODE_OSU;
|
||||
replay.version = Updater.get().getBuildDate();
|
||||
replay.beatmapHash = ""; // TODO
|
||||
replay.playerName = ""; // TODO
|
||||
replay.replayHash = ""; // 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
|
||||
return replay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not this object is used for gameplay.
|
||||
* @return true if gameplay, false if score viewing
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
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);
|
||||
}
|
||||
}
|
|
@ -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,9 @@ public class Updater {
|
|||
/** The current and latest versions. */
|
||||
private DefaultArtifactVersion currentVersion, latestVersion;
|
||||
|
||||
/** The build date. */
|
||||
private int buildDate = -1;
|
||||
|
||||
/** The download object. */
|
||||
private Download download;
|
||||
|
||||
|
@ -115,6 +122,32 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the version from a set of properties.
|
||||
* @param props the set of properties
|
||||
|
|
|
@ -18,15 +18,26 @@
|
|||
|
||||
package itdelatrisu.opsu.replay;
|
||||
|
||||
import itdelatrisu.opsu.ErrorHandler;
|
||||
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;
|
||||
|
||||
|
@ -59,7 +70,7 @@ public class Replay {
|
|||
public short hit300, hit100, hit50, geki, katu, miss;
|
||||
|
||||
/** The score. */
|
||||
public long score;
|
||||
public int score;
|
||||
|
||||
/** The max combo. */
|
||||
public short combo;
|
||||
|
@ -82,19 +93,36 @@ public class Replay {
|
|||
/** 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.
|
||||
*/
|
||||
public void load() {
|
||||
try {
|
||||
OsuReader reader = new OsuReader(file);
|
||||
loadHeader(reader);
|
||||
loadData(reader);
|
||||
reader.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
ErrorHandler.error("Could not load replay data.", e, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,8 +166,8 @@ public class Replay {
|
|||
int time = Integer.parseInt(tokens[0]);
|
||||
float percentage = Float.parseFloat(tokens[1]);
|
||||
lifeFrameList.add(new LifeFrame(time, percentage));
|
||||
} catch (NumberFormatException | NullPointerException e) {
|
||||
Log.warn(String.format("Failed to life frame: '%s'", frame), e);
|
||||
} catch (NumberFormatException e) {
|
||||
Log.warn(String.format("Failed to load life frame: '%s'", frame), e);
|
||||
}
|
||||
}
|
||||
this.lifeFrames = lifeFrameList.toArray(new LifeFrame[lifeFrameList.size()]);
|
||||
|
@ -152,6 +180,7 @@ public class Replay {
|
|||
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) {
|
||||
|
@ -161,6 +190,10 @@ public class Replay {
|
|||
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]);
|
||||
|
@ -168,7 +201,7 @@ public class Replay {
|
|||
int keys = Integer.parseInt(tokens[3]);
|
||||
replayFrameList.add(new ReplayFrame(timeDiff, time, x, y, keys));
|
||||
lastTime = time;
|
||||
} catch (NumberFormatException | NullPointerException e) {
|
||||
} catch (NumberFormatException e) {
|
||||
Log.warn(String.format("Failed to parse frame: '%s'", frame), e);
|
||||
}
|
||||
}
|
||||
|
@ -176,9 +209,94 @@ public class Replay {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the replay data to a file.
|
||||
* @param file the file to write to
|
||||
*/
|
||||
public void save(File file) {
|
||||
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.getRawX()),
|
||||
nf.format(frame.getRawY()), 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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final int LINE_SPLIT = 10;
|
||||
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');
|
||||
|
@ -197,8 +315,8 @@ public class Replay {
|
|||
sb.append("Max combo: "); sb.append(combo); sb.append('\n');
|
||||
sb.append("Perfect: "); sb.append(perfect); sb.append('\n');
|
||||
sb.append("Mods: "); sb.append(mods); sb.append('\n');
|
||||
sb.append("Life data:\n");
|
||||
for (int i = 0; i < lifeFrames.length; i++) {
|
||||
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]);
|
||||
|
@ -208,14 +326,16 @@ public class Replay {
|
|||
sb.append("Timestamp: "); sb.append(timestamp); sb.append('\n');
|
||||
sb.append("Replay length: "); sb.append(replayLength); sb.append('\n');
|
||||
if (frames != null) {
|
||||
sb.append("Frames:\n");
|
||||
for (int i = 0; i < frames.length; i++) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,10 +78,20 @@ public class ReplayFrame {
|
|||
public int getX() { return (int) (x * OsuHitObject.getXMultiplier() + OsuHitObject.getXOffset()); }
|
||||
|
||||
/**
|
||||
* Returns the scaled cursor Y coordinate.
|
||||
* Returns the scaled cursor y coordinate.
|
||||
*/
|
||||
public int getY() { return (int) (y * OsuHitObject.getYMultiplier() + OsuHitObject.getYOffset()); }
|
||||
|
||||
/**
|
||||
* Returns the raw cursor x coordinate.
|
||||
*/
|
||||
public float getRawX() { return x; }
|
||||
|
||||
/**
|
||||
* Returns the raw cursor y coordinate.
|
||||
*/
|
||||
public float getRawY() { return y; }
|
||||
|
||||
/**
|
||||
* Returns the keys pressed (KEY_* bitmask).
|
||||
*/
|
||||
|
|
|
@ -44,6 +44,7 @@ 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;
|
||||
|
||||
|
@ -175,6 +176,9 @@ public class Game extends BasicGameState {
|
|||
/** The previous game mod state (before the replay). */
|
||||
private int previousMods = 0;
|
||||
|
||||
/** The list of current replay frames (for recording replays). */
|
||||
private LinkedList<ReplayFrame> frameList;
|
||||
|
||||
// game-related variables
|
||||
private GameContainer container;
|
||||
private StateBasedGame game;
|
||||
|
@ -468,7 +472,8 @@ public class Game extends BasicGameState {
|
|||
game.closeRequested();
|
||||
else { // go to ranking screen
|
||||
((GameRanking) game.getState(Opsu.STATE_GAMERANKING)).setGameData(data);
|
||||
ScoreData score = data.getScoreData(osu);
|
||||
ScoreData score = data.getScoreData(osu, (frameList == null) ? null :
|
||||
frameList.toArray(new ReplayFrame[frameList.size()]));
|
||||
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));
|
||||
|
@ -893,7 +898,8 @@ public class Game extends BasicGameState {
|
|||
};
|
||||
replayThreadRunning = true;
|
||||
replayThread.start();
|
||||
}
|
||||
} else
|
||||
frameList = new LinkedList<ReplayFrame>();
|
||||
|
||||
leadInTime = osu.audioLeadIn + approachTime;
|
||||
restart = Restart.FALSE;
|
||||
|
@ -938,6 +944,7 @@ public class Game extends BasicGameState {
|
|||
checkpointLoaded = false;
|
||||
deaths = 0;
|
||||
deathTime = -1;
|
||||
frameList = null;
|
||||
|
||||
System.gc();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user