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>
|
<artifactId>commons-compress</artifactId>
|
||||||
<version>1.8</version>
|
<version>1.8</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.jponge</groupId>
|
||||||
|
<artifactId>lzma-java</artifactId>
|
||||||
|
<version>1.2</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|
|
@ -22,10 +22,13 @@ import itdelatrisu.opsu.audio.HitSound;
|
||||||
import itdelatrisu.opsu.audio.MusicController;
|
import itdelatrisu.opsu.audio.MusicController;
|
||||||
import itdelatrisu.opsu.audio.SoundController;
|
import itdelatrisu.opsu.audio.SoundController;
|
||||||
import itdelatrisu.opsu.audio.SoundEffect;
|
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.HashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.concurrent.LinkedBlockingDeque;
|
import java.util.concurrent.LinkedBlockingDeque;
|
||||||
|
|
||||||
import org.newdawn.slick.Animation;
|
import org.newdawn.slick.Animation;
|
||||||
|
@ -275,6 +278,9 @@ public class GameData {
|
||||||
/** The associated score data. */
|
/** The associated score data. */
|
||||||
private ScoreData scoreData;
|
private ScoreData scoreData;
|
||||||
|
|
||||||
|
/** The associated replay. */
|
||||||
|
private Replay replay;
|
||||||
|
|
||||||
/** Whether this object is used for gameplay (true) or score viewing (false). */
|
/** Whether this object is used for gameplay (true) or score viewing (false). */
|
||||||
private boolean gameplay;
|
private boolean gameplay;
|
||||||
|
|
||||||
|
@ -319,6 +325,7 @@ public class GameData {
|
||||||
hitResultCount[HIT_300K] = 0;
|
hitResultCount[HIT_300K] = 0;
|
||||||
hitResultCount[HIT_100K] = s.katu;
|
hitResultCount[HIT_100K] = s.katu;
|
||||||
hitResultCount[HIT_MISS] = s.miss;
|
hitResultCount[HIT_MISS] = s.miss;
|
||||||
|
this.replay = s.replay;
|
||||||
|
|
||||||
loadImages();
|
loadImages();
|
||||||
}
|
}
|
||||||
|
@ -342,6 +349,7 @@ public class GameData {
|
||||||
comboEnd = 0;
|
comboEnd = 0;
|
||||||
comboBurstIndex = -1;
|
comboBurstIndex = -1;
|
||||||
scoreData = null;
|
scoreData = null;
|
||||||
|
replay = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1190,9 +1198,10 @@ public class GameData {
|
||||||
* If score data already exists, the existing object will be returned
|
* If score data already exists, the existing object will be returned
|
||||||
* (i.e. this will not overwrite existing data).
|
* (i.e. this will not overwrite existing data).
|
||||||
* @param osu the OsuFile
|
* @param osu the OsuFile
|
||||||
|
* @param frames the replay frames
|
||||||
* @return the ScoreData object
|
* @return the ScoreData object
|
||||||
*/
|
*/
|
||||||
public ScoreData getScoreData(OsuFile osu) {
|
public ScoreData getScoreData(OsuFile osu, ReplayFrame[] frames) {
|
||||||
if (scoreData != null)
|
if (scoreData != null)
|
||||||
return scoreData;
|
return scoreData;
|
||||||
|
|
||||||
|
@ -1214,9 +1223,44 @@ public class GameData {
|
||||||
scoreData.combo = comboMax;
|
scoreData.combo = comboMax;
|
||||||
scoreData.perfect = (comboMax == fullObjectCount);
|
scoreData.perfect = (comboMax == fullObjectCount);
|
||||||
scoreData.mods = GameMod.getModState();
|
scoreData.mods = GameMod.getModState();
|
||||||
|
scoreData.replay = getReplay(frames);
|
||||||
return scoreData;
|
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.
|
* Returns whether or not this object is used for gameplay.
|
||||||
* @return true if gameplay, false if score viewing
|
* @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.
|
* Data type storing parsed data from OSU files.
|
||||||
*/
|
*/
|
||||||
public class OsuFile implements Comparable<OsuFile> {
|
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. */
|
/** Map of all loaded background images. */
|
||||||
private static HashMap<OsuFile, Image> bgImageMap = new HashMap<OsuFile, Image>();
|
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. */
|
/** How often closely placed hit objects will be stacked together. */
|
||||||
public float stackLeniency = 0.7f;
|
public float stackLeniency = 0.7f;
|
||||||
|
|
||||||
/** Game mode (0:osu!, 1:taiko, 2:catch the beat, 3:osu!mania). */
|
/** Game mode (MODE_* constants). */
|
||||||
public byte mode = 0;
|
public byte mode = MODE_OSU;
|
||||||
|
|
||||||
/** Whether the letterbox (top/bottom black bars) appears during breaks. */
|
/** Whether the letterbox (top/bottom black bars) appears during breaks. */
|
||||||
public boolean letterboxInBreaks = false;
|
public boolean letterboxInBreaks = false;
|
||||||
|
|
|
@ -272,7 +272,7 @@ public class OsuParser {
|
||||||
osu.mode = Byte.parseByte(tokens[1]);
|
osu.mode = Byte.parseByte(tokens[1]);
|
||||||
|
|
||||||
/* Non-Opsu! standard files not implemented (obviously). */
|
/* Non-Opsu! standard files not implemented (obviously). */
|
||||||
if (osu.mode != 0)
|
if (osu.mode != OsuFile.MODE_OSU)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
break;
|
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.IOException;
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.net.URL;
|
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 java.util.Properties;
|
||||||
|
|
||||||
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
|
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
|
||||||
|
@ -93,6 +97,9 @@ public class Updater {
|
||||||
/** The current and latest versions. */
|
/** The current and latest versions. */
|
||||||
private DefaultArtifactVersion currentVersion, latestVersion;
|
private DefaultArtifactVersion currentVersion, latestVersion;
|
||||||
|
|
||||||
|
/** The build date. */
|
||||||
|
private int buildDate = -1;
|
||||||
|
|
||||||
/** The download object. */
|
/** The download object. */
|
||||||
private Download download;
|
private Download download;
|
||||||
|
|
||||||
|
@ -115,6 +122,32 @@ public class Updater {
|
||||||
return (status == Status.UPDATE_AVAILABLE || status == Status.UPDATE_DOWNLOADED || status == Status.UPDATE_DOWNLOADING);
|
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.
|
* Returns the version from a set of properties.
|
||||||
* @param props the set of properties
|
* @param props the set of properties
|
||||||
|
|
|
@ -18,15 +18,26 @@
|
||||||
|
|
||||||
package itdelatrisu.opsu.replay;
|
package itdelatrisu.opsu.replay;
|
||||||
|
|
||||||
|
import itdelatrisu.opsu.ErrorHandler;
|
||||||
import itdelatrisu.opsu.OsuReader;
|
import itdelatrisu.opsu.OsuReader;
|
||||||
|
import itdelatrisu.opsu.OsuWriter;
|
||||||
import itdelatrisu.opsu.Utils;
|
import itdelatrisu.opsu.Utils;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
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.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import lzma.streams.LzmaOutputStream;
|
||||||
|
|
||||||
import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream;
|
import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream;
|
||||||
import org.newdawn.slick.util.Log;
|
import org.newdawn.slick.util.Log;
|
||||||
|
|
||||||
|
@ -59,7 +70,7 @@ public class Replay {
|
||||||
public short hit300, hit100, hit50, geki, katu, miss;
|
public short hit300, hit100, hit50, geki, katu, miss;
|
||||||
|
|
||||||
/** The score. */
|
/** The score. */
|
||||||
public long score;
|
public int score;
|
||||||
|
|
||||||
/** The max combo. */
|
/** The max combo. */
|
||||||
public short combo;
|
public short combo;
|
||||||
|
@ -82,19 +93,36 @@ public class Replay {
|
||||||
/** Replay frames. */
|
/** Replay frames. */
|
||||||
public ReplayFrame[] frames;
|
public ReplayFrame[] frames;
|
||||||
|
|
||||||
|
/** Seed. (?) */
|
||||||
|
public int seed;
|
||||||
|
|
||||||
|
/** Seed string. */
|
||||||
|
private static final String SEED_STRING = "-12345";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty constructor.
|
||||||
|
*/
|
||||||
|
public Replay() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
* @param file the file to load from
|
* @param file the file to load from
|
||||||
*/
|
*/
|
||||||
public Replay(File file) {
|
public Replay(File file) {
|
||||||
this.file = file;
|
this.file = file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the replay data.
|
||||||
|
*/
|
||||||
|
public void load() {
|
||||||
try {
|
try {
|
||||||
OsuReader reader = new OsuReader(file);
|
OsuReader reader = new OsuReader(file);
|
||||||
loadHeader(reader);
|
loadHeader(reader);
|
||||||
loadData(reader);
|
loadData(reader);
|
||||||
reader.close();
|
reader.close();
|
||||||
} catch (IOException e) {
|
} 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]);
|
int time = Integer.parseInt(tokens[0]);
|
||||||
float percentage = Float.parseFloat(tokens[1]);
|
float percentage = Float.parseFloat(tokens[1]);
|
||||||
lifeFrameList.add(new LifeFrame(time, percentage));
|
lifeFrameList.add(new LifeFrame(time, percentage));
|
||||||
} catch (NumberFormatException | NullPointerException e) {
|
} catch (NumberFormatException e) {
|
||||||
Log.warn(String.format("Failed to life frame: '%s'", frame), e);
|
Log.warn(String.format("Failed to load life frame: '%s'", frame), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.lifeFrames = lifeFrameList.toArray(new LifeFrame[lifeFrameList.size()]);
|
this.lifeFrames = lifeFrameList.toArray(new LifeFrame[lifeFrameList.size()]);
|
||||||
|
@ -152,6 +180,7 @@ public class Replay {
|
||||||
if (replayLength > 0) {
|
if (replayLength > 0) {
|
||||||
LZMACompressorInputStream lzma = new LZMACompressorInputStream(reader.getInputStream());
|
LZMACompressorInputStream lzma = new LZMACompressorInputStream(reader.getInputStream());
|
||||||
String[] replayFrames = Utils.convertStreamToString(lzma).split(",");
|
String[] replayFrames = Utils.convertStreamToString(lzma).split(",");
|
||||||
|
lzma.close();
|
||||||
List<ReplayFrame> replayFrameList = new ArrayList<ReplayFrame>(replayFrames.length);
|
List<ReplayFrame> replayFrameList = new ArrayList<ReplayFrame>(replayFrames.length);
|
||||||
int lastTime = 0;
|
int lastTime = 0;
|
||||||
for (String frame : replayFrames) {
|
for (String frame : replayFrames) {
|
||||||
|
@ -161,6 +190,10 @@ public class Replay {
|
||||||
if (tokens.length < 4)
|
if (tokens.length < 4)
|
||||||
continue;
|
continue;
|
||||||
try {
|
try {
|
||||||
|
if (tokens[0].equals(SEED_STRING)) {
|
||||||
|
seed = Integer.parseInt(tokens[3]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
int timeDiff = Integer.parseInt(tokens[0]);
|
int timeDiff = Integer.parseInt(tokens[0]);
|
||||||
int time = timeDiff + lastTime;
|
int time = timeDiff + lastTime;
|
||||||
float x = Float.parseFloat(tokens[1]);
|
float x = Float.parseFloat(tokens[1]);
|
||||||
|
@ -168,7 +201,7 @@ public class Replay {
|
||||||
int keys = Integer.parseInt(tokens[3]);
|
int keys = Integer.parseInt(tokens[3]);
|
||||||
replayFrameList.add(new ReplayFrame(timeDiff, time, x, y, keys));
|
replayFrameList.add(new ReplayFrame(timeDiff, time, x, y, keys));
|
||||||
lastTime = time;
|
lastTime = time;
|
||||||
} catch (NumberFormatException | NullPointerException e) {
|
} catch (NumberFormatException e) {
|
||||||
Log.warn(String.format("Failed to parse frame: '%s'", frame), 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
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
final int LINE_SPLIT = 10;
|
final int LINE_SPLIT = 5;
|
||||||
|
final int MAX_LINES = LINE_SPLIT * 10;
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append("File: "); sb.append(file.getName()); sb.append('\n');
|
sb.append("File: "); sb.append(file.getName()); sb.append('\n');
|
||||||
sb.append("Mode: "); sb.append(mode); 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("Max combo: "); sb.append(combo); sb.append('\n');
|
||||||
sb.append("Perfect: "); sb.append(perfect); sb.append('\n');
|
sb.append("Perfect: "); sb.append(perfect); sb.append('\n');
|
||||||
sb.append("Mods: "); sb.append(mods); sb.append('\n');
|
sb.append("Mods: "); sb.append(mods); sb.append('\n');
|
||||||
sb.append("Life data:\n");
|
sb.append("Life data ("); sb.append(lifeFrames.length); sb.append(" total):\n");
|
||||||
for (int i = 0; i < lifeFrames.length; i++) {
|
for (int i = 0; i < lifeFrames.length && i < MAX_LINES; i++) {
|
||||||
if (i % LINE_SPLIT == 0)
|
if (i % LINE_SPLIT == 0)
|
||||||
sb.append('\t');
|
sb.append('\t');
|
||||||
sb.append(lifeFrames[i]);
|
sb.append(lifeFrames[i]);
|
||||||
|
@ -208,14 +326,16 @@ public class Replay {
|
||||||
sb.append("Timestamp: "); sb.append(timestamp); sb.append('\n');
|
sb.append("Timestamp: "); sb.append(timestamp); sb.append('\n');
|
||||||
sb.append("Replay length: "); sb.append(replayLength); sb.append('\n');
|
sb.append("Replay length: "); sb.append(replayLength); sb.append('\n');
|
||||||
if (frames != null) {
|
if (frames != null) {
|
||||||
sb.append("Frames:\n");
|
sb.append("Frames ("); sb.append(frames.length); sb.append(" total):\n");
|
||||||
for (int i = 0; i < frames.length; i++) {
|
for (int i = 0; i < frames.length && i < MAX_LINES; i++) {
|
||||||
if (i % LINE_SPLIT == 0)
|
if (i % LINE_SPLIT == 0)
|
||||||
sb.append('\t');
|
sb.append('\t');
|
||||||
sb.append(frames[i]);
|
sb.append(frames[i]);
|
||||||
sb.append((i % LINE_SPLIT == LINE_SPLIT - 1) ? '\n' : ' ');
|
sb.append((i % LINE_SPLIT == LINE_SPLIT - 1) ? '\n' : ' ');
|
||||||
}
|
}
|
||||||
|
sb.append('\n');
|
||||||
}
|
}
|
||||||
|
sb.append("Seed: "); sb.append(seed); sb.append('\n');
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,10 +78,20 @@ public class ReplayFrame {
|
||||||
public int getX() { return (int) (x * OsuHitObject.getXMultiplier() + OsuHitObject.getXOffset()); }
|
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()); }
|
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).
|
* Returns the keys pressed (KEY_* bitmask).
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -44,6 +44,7 @@ import itdelatrisu.opsu.replay.Replay;
|
||||||
import itdelatrisu.opsu.replay.ReplayFrame;
|
import itdelatrisu.opsu.replay.ReplayFrame;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.Stack;
|
import java.util.Stack;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@ -175,6 +176,9 @@ public class Game extends BasicGameState {
|
||||||
/** The previous game mod state (before the replay). */
|
/** The previous game mod state (before the replay). */
|
||||||
private int previousMods = 0;
|
private int previousMods = 0;
|
||||||
|
|
||||||
|
/** The list of current replay frames (for recording replays). */
|
||||||
|
private LinkedList<ReplayFrame> frameList;
|
||||||
|
|
||||||
// game-related variables
|
// game-related variables
|
||||||
private GameContainer container;
|
private GameContainer container;
|
||||||
private StateBasedGame game;
|
private StateBasedGame game;
|
||||||
|
@ -468,7 +472,8 @@ public class Game extends BasicGameState {
|
||||||
game.closeRequested();
|
game.closeRequested();
|
||||||
else { // go to ranking screen
|
else { // go to ranking screen
|
||||||
((GameRanking) game.getState(Opsu.STATE_GAMERANKING)).setGameData(data);
|
((GameRanking) game.getState(Opsu.STATE_GAMERANKING)).setGameData(data);
|
||||||
ScoreData score = data.getScoreData(osu);
|
ScoreData score = data.getScoreData(osu, (frameList == null) ? null :
|
||||||
|
frameList.toArray(new ReplayFrame[frameList.size()]));
|
||||||
if (!GameMod.AUTO.isActive() && !GameMod.RELAX.isActive() && !GameMod.AUTOPILOT.isActive() && !isReplay)
|
if (!GameMod.AUTO.isActive() && !GameMod.RELAX.isActive() && !GameMod.AUTOPILOT.isActive() && !isReplay)
|
||||||
ScoreDB.addScore(score);
|
ScoreDB.addScore(score);
|
||||||
game.enterState(Opsu.STATE_GAMERANKING, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
game.enterState(Opsu.STATE_GAMERANKING, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||||
|
@ -893,7 +898,8 @@ public class Game extends BasicGameState {
|
||||||
};
|
};
|
||||||
replayThreadRunning = true;
|
replayThreadRunning = true;
|
||||||
replayThread.start();
|
replayThread.start();
|
||||||
}
|
} else
|
||||||
|
frameList = new LinkedList<ReplayFrame>();
|
||||||
|
|
||||||
leadInTime = osu.audioLeadIn + approachTime;
|
leadInTime = osu.audioLeadIn + approachTime;
|
||||||
restart = Restart.FALSE;
|
restart = Restart.FALSE;
|
||||||
|
@ -938,6 +944,7 @@ public class Game extends BasicGameState {
|
||||||
checkpointLoaded = false;
|
checkpointLoaded = false;
|
||||||
deaths = 0;
|
deaths = 0;
|
||||||
deathTime = -1;
|
deathTime = -1;
|
||||||
|
frameList = null;
|
||||||
|
|
||||||
System.gc();
|
System.gc();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user