Initial replay parsing support.
- Basic implementation of viewing replays in the Game state. - Added OsuReader class for reading certain osu! file types. (author: Markus Jarderot) - Added Replay, ReplayFrame, and LifeFrame classes to capture replay data. (author: smoogipooo) - Added 'keyPressed' parameter to HitObject.update(). - Added cursor-drawing methods in UI that take the mouse coordinates and pressed state as parameters. - Added GameMod methods to retrieve/load the active mods state as a bitmask. - Added Apache commons-compress dependency for handling LZMA decompression. Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
This commit is contained in:
57
src/itdelatrisu/opsu/replay/LifeFrame.java
Normal file
57
src/itdelatrisu/opsu/replay/LifeFrame.java
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* opsu! - an open-source osu! client
|
||||
* Copyright (C) 2014, 2015 Jeffrey Han
|
||||
*
|
||||
* opsu! is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* opsu! is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Captures a single life frame.
|
||||
*
|
||||
* @author smoogipooo (https://github.com/smoogipooo/osu-Replay-API/)
|
||||
*/
|
||||
package itdelatrisu.opsu.replay;
|
||||
|
||||
public class LifeFrame {
|
||||
/** Time. */
|
||||
private int time;
|
||||
|
||||
/** Percentage. */
|
||||
private float percentage;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param time the time
|
||||
* @param percentage the percentage
|
||||
*/
|
||||
public LifeFrame(int time, float percentage) {
|
||||
this.time = time;
|
||||
this.percentage = percentage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the frame time.
|
||||
*/
|
||||
public int getTime() { return time; }
|
||||
|
||||
/**
|
||||
* Returns the frame percentage.
|
||||
*/
|
||||
public float getPercentage() { return percentage; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("(%d, %.2f)", time, percentage);
|
||||
}
|
||||
}
|
||||
221
src/itdelatrisu/opsu/replay/Replay.java
Normal file
221
src/itdelatrisu/opsu/replay/Replay.java
Normal file
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* opsu! - an open-source osu! client
|
||||
* Copyright (C) 2014, 2015 Jeffrey Han
|
||||
*
|
||||
* opsu! is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* opsu! is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu.replay;
|
||||
|
||||
import itdelatrisu.opsu.OsuReader;
|
||||
import itdelatrisu.opsu.Utils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream;
|
||||
import org.newdawn.slick.util.Log;
|
||||
|
||||
/**
|
||||
* Captures osu! replay data.
|
||||
* https://osu.ppy.sh/wiki/Osr_%28file_format%29
|
||||
*
|
||||
* @author smoogipooo (https://github.com/smoogipooo/osu-Replay-API/)
|
||||
*/
|
||||
public class Replay {
|
||||
/** The associated file. */
|
||||
private File file;
|
||||
|
||||
/** The game mode. */
|
||||
public byte mode;
|
||||
|
||||
/** Game version when the replay was created. */
|
||||
public int version;
|
||||
|
||||
/** Beatmap MD5 hash. */
|
||||
public String beatmapHash;
|
||||
|
||||
/** The player's name. */
|
||||
public String playerName;
|
||||
|
||||
/** Replay MD5 hash. */
|
||||
public String replayHash;
|
||||
|
||||
/** Hit result counts. */
|
||||
public short hit300, hit100, hit50, geki, katu, miss;
|
||||
|
||||
/** The score. */
|
||||
public long score;
|
||||
|
||||
/** The max combo. */
|
||||
public short combo;
|
||||
|
||||
/** Whether or not a full combo was achieved. */
|
||||
public boolean perfect;
|
||||
|
||||
/** Game mod bitmask. */
|
||||
public int mods;
|
||||
|
||||
/** Life frames. */
|
||||
public LifeFrame[] lifeFrames;
|
||||
|
||||
/** The time when the replay was created. */
|
||||
public Date timestamp;
|
||||
|
||||
/** Length of the replay data. */
|
||||
public int replayLength;
|
||||
|
||||
/** Replay frames. */
|
||||
public ReplayFrame[] frames;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param file the file to load from
|
||||
*/
|
||||
public Replay(File file) {
|
||||
this.file = file;
|
||||
try {
|
||||
OsuReader reader = new OsuReader(file);
|
||||
loadHeader(reader);
|
||||
loadData(reader);
|
||||
reader.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the replay header data.
|
||||
* @param reader the associated reader
|
||||
* @throws IOException
|
||||
*/
|
||||
private void loadHeader(OsuReader reader) throws IOException {
|
||||
this.mode = reader.readByte();
|
||||
this.version = reader.readInt();
|
||||
this.beatmapHash = reader.readString();
|
||||
this.playerName = reader.readString();
|
||||
this.replayHash = reader.readString();
|
||||
this.hit300 = reader.readShort();
|
||||
this.hit100 = reader.readShort();
|
||||
this.hit50 = reader.readShort();
|
||||
this.geki = reader.readShort();
|
||||
this.katu = reader.readShort();
|
||||
this.miss = reader.readShort();
|
||||
this.score = reader.readInt();
|
||||
this.combo = reader.readShort();
|
||||
this.perfect = reader.readBoolean();
|
||||
this.mods = reader.readInt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the replay data.
|
||||
* @param reader the associated reader
|
||||
* @throws IOException
|
||||
*/
|
||||
private void loadData(OsuReader reader) throws IOException {
|
||||
// life data
|
||||
String[] lifeData = reader.readString().split(",");
|
||||
List<LifeFrame> lifeFrameList = new ArrayList<LifeFrame>(lifeData.length);
|
||||
for (String frame : lifeData) {
|
||||
String[] tokens = frame.split("\\|");
|
||||
if (tokens.length < 2)
|
||||
continue;
|
||||
try {
|
||||
int time = Integer.parseInt(tokens[0]);
|
||||
float percentage = Float.parseFloat(tokens[1]);
|
||||
lifeFrameList.add(new LifeFrame(time, percentage));
|
||||
} catch (NumberFormatException | NullPointerException e) {
|
||||
Log.warn(String.format("Failed to life frame: '%s'", frame), e);
|
||||
}
|
||||
}
|
||||
this.lifeFrames = lifeFrameList.toArray(new LifeFrame[lifeFrameList.size()]);
|
||||
|
||||
// timestamp
|
||||
this.timestamp = reader.readDate();
|
||||
|
||||
// LZMA-encoded replay data
|
||||
this.replayLength = reader.readInt();
|
||||
if (replayLength > 0) {
|
||||
LZMACompressorInputStream lzma = new LZMACompressorInputStream(reader.getInputStream());
|
||||
String[] replayFrames = Utils.convertStreamToString(lzma).split(",");
|
||||
List<ReplayFrame> replayFrameList = new ArrayList<ReplayFrame>(replayFrames.length);
|
||||
int lastTime = 0;
|
||||
for (String frame : replayFrames) {
|
||||
if (frame.isEmpty())
|
||||
continue;
|
||||
String[] tokens = frame.split("\\|");
|
||||
if (tokens.length < 4)
|
||||
continue;
|
||||
try {
|
||||
int timeDiff = Integer.parseInt(tokens[0]);
|
||||
int time = timeDiff + lastTime;
|
||||
float x = Float.parseFloat(tokens[1]);
|
||||
float y = Float.parseFloat(tokens[2]);
|
||||
int keys = Integer.parseInt(tokens[3]);
|
||||
replayFrameList.add(new ReplayFrame(timeDiff, time, x, y, keys));
|
||||
lastTime = time;
|
||||
} catch (NumberFormatException | NullPointerException e) {
|
||||
Log.warn(String.format("Failed to parse frame: '%s'", frame), e);
|
||||
}
|
||||
}
|
||||
this.frames = replayFrameList.toArray(new ReplayFrame[replayFrameList.size()]);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final int LINE_SPLIT = 10;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("File: "); sb.append(file.getName()); sb.append('\n');
|
||||
sb.append("Mode: "); sb.append(mode); sb.append('\n');
|
||||
sb.append("Version: "); sb.append(version); sb.append('\n');
|
||||
sb.append("Beatmap hash: "); sb.append(beatmapHash); sb.append('\n');
|
||||
sb.append("Player name: "); sb.append(playerName); sb.append('\n');
|
||||
sb.append("Replay hash: "); sb.append(replayHash); sb.append('\n');
|
||||
sb.append("Hits: ");
|
||||
sb.append(hit300); sb.append(' ');
|
||||
sb.append(hit100); sb.append(' ');
|
||||
sb.append(hit50); sb.append(' ');
|
||||
sb.append(geki); sb.append(' ');
|
||||
sb.append(katu); sb.append(' ');
|
||||
sb.append(miss); sb.append('\n');
|
||||
sb.append("Score: "); sb.append(score); sb.append('\n');
|
||||
sb.append("Max combo: "); sb.append(combo); sb.append('\n');
|
||||
sb.append("Perfect: "); sb.append(perfect); sb.append('\n');
|
||||
sb.append("Mods: "); sb.append(mods); sb.append('\n');
|
||||
sb.append("Life data:\n");
|
||||
for (int i = 0; i < lifeFrames.length; i++) {
|
||||
if (i % LINE_SPLIT == 0)
|
||||
sb.append('\t');
|
||||
sb.append(lifeFrames[i]);
|
||||
sb.append((i % LINE_SPLIT == LINE_SPLIT - 1) ? '\n' : ' ');
|
||||
}
|
||||
sb.append('\n');
|
||||
sb.append("Timestamp: "); sb.append(timestamp); sb.append('\n');
|
||||
sb.append("Replay length: "); sb.append(replayLength); sb.append('\n');
|
||||
if (frames != null) {
|
||||
sb.append("Frames:\n");
|
||||
for (int i = 0; i < frames.length; i++) {
|
||||
if (i % LINE_SPLIT == 0)
|
||||
sb.append('\t');
|
||||
sb.append(frames[i]);
|
||||
sb.append((i % LINE_SPLIT == LINE_SPLIT - 1) ? '\n' : ' ');
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
99
src/itdelatrisu/opsu/replay/ReplayFrame.java
Normal file
99
src/itdelatrisu/opsu/replay/ReplayFrame.java
Normal file
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* opsu! - an open-source osu! client
|
||||
* Copyright (C) 2014, 2015 Jeffrey Han
|
||||
*
|
||||
* opsu! is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* opsu! is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu.replay;
|
||||
|
||||
import itdelatrisu.opsu.OsuHitObject;
|
||||
|
||||
/**
|
||||
* Captures a single replay frame.
|
||||
*
|
||||
* @author smoogipooo (https://github.com/smoogipooo/osu-Replay-API/)
|
||||
*/
|
||||
public class ReplayFrame {
|
||||
/** Key bits. */
|
||||
public static final int
|
||||
KEY_NONE = 0,
|
||||
KEY_M1 = (1 << 0),
|
||||
KEY_M2 = (1 << 1),
|
||||
KEY_K1 = (1 << 2) | (1 << 0),
|
||||
KEY_K2 = (1 << 3) | (1 << 1);
|
||||
|
||||
/** Time, in milliseconds, since the previous action. */
|
||||
private int timeDiff;
|
||||
|
||||
/** Time, in milliseconds. */
|
||||
private int time;
|
||||
|
||||
/** Cursor coordinates (in OsuPixels). */
|
||||
private float x, y;
|
||||
|
||||
/** Keys pressed (bitmask). */
|
||||
private int keys;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param timeDiff time since the previous action (in ms)
|
||||
* @param time time (in ms)
|
||||
* @param x cursor x coordinate [0, 512]
|
||||
* @param y cursor y coordinate [0, 384]
|
||||
* @param keys keys pressed (bitmask)
|
||||
*/
|
||||
public ReplayFrame(int timeDiff, int time, float x, float y, int keys) {
|
||||
this.timeDiff = timeDiff;
|
||||
this.time = time;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.keys = keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the frame time, in milliseconds.
|
||||
*/
|
||||
public int getTime() { return time; }
|
||||
|
||||
/**
|
||||
* Returns the time since the previous action, in milliseconds.
|
||||
*/
|
||||
public int getTimeDiff() { return timeDiff; }
|
||||
|
||||
/**
|
||||
* Returns the scaled cursor x coordinate.
|
||||
*/
|
||||
public int getX() { return (int) (x * OsuHitObject.getXMultiplier() + OsuHitObject.getXOffset()); }
|
||||
|
||||
/**
|
||||
* Returns the scaled cursor Y coordinate.
|
||||
*/
|
||||
public int getY() { return (int) (y * OsuHitObject.getYMultiplier() + OsuHitObject.getYOffset()); }
|
||||
|
||||
/**
|
||||
* Returns the keys pressed (KEY_* bitmask).
|
||||
*/
|
||||
public int getKeys() { return keys; }
|
||||
|
||||
/**
|
||||
* Returns whether or not a key is pressed.
|
||||
*/
|
||||
public boolean isKeyPressed() { return (keys != KEY_NONE); }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("(%d, [%.2f, %.2f], %d)", time, x, y, keys);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user