Task 488: .OSR File Format
Task 488: .OSR File Format
File Format Specifications for .OSR
The .OSR file format is used by the rhythm game osu! to store replay data. It is a binary format with a header containing fixed and variable-length fields, followed by LZMA-compressed replay action data, and an optional trailing field. All multi-byte integers are stored in little-endian order. Strings use a special format: a byte (0x00 for empty, 0x0b for present), followed by a ULEB128 length and UTF-8 encoded string if present. The replay data, when decompressed, is a CSV-like text stream of actions in the format w|x|y|z, with an optional trailing RNG seed frame for newer versions.
List of all properties (fields) intrinsic to the file format:
- Game mode (byte): 0 = osu!, 1 = osu!taiko, 2 = osu!catch, 3 = osu!mania.
- Game version (int): The osu! version when the replay was created (e.g., 20131216).
- Beatmap MD5 hash (string): MD5 hash of the beatmap file.
- Player name (string): Name of the player who set the replay.
- Replay MD5 hash (string): MD5 hash of replay properties for verification.
- Number of 300s (short): Count of perfect hits (300s).
- Number of 100s (short): Count of 100s (or equivalents in other modes).
- Number of 50s (short): Count of 50s (or equivalents).
- Number of Gekis (short): Count of Gekis (or Max 300s in mania).
- Number of Katus (short): Count of Katus (or 200s in mania).
- Number of misses (short): Count of misses.
- Total score (int): Score displayed on the report.
- Max combo (short): Greatest combo displayed.
- Perfect/full combo (byte): 1 if perfect (no misses/slider breaks), 0 otherwise.
- Mods used (int): Bitwise flags for mods (e.g., 64 for DoubleTime).
- Life bar graph (string): Comma-separated
time|healthpairs (time in ms, health 0-1 float). - Timestamp (long): Windows ticks since 0001-01-01.
- Compressed replay length (int): Byte length of the LZMA-compressed replay data.
- Compressed replay data (byte array): LZMA-compressed stream of actions (decompresses to text like
w|x|y|zper frame, where w=delta time ms (long), x=cursor X (float), y=cursor Y (float), z=keys pressed (int bitwise)). - Online score ID (long): ID for online leaderboard.
- Additional mod info (double, optional): Present if Target Practice mod is used; stores total accuracy.
Two direct download links for .OSR files:
Ghost blog embedded HTML/JavaScript for drag-and-drop .OSR file dump:
<div id="drop-area" style="border: 2px dashed #ccc; padding: 20px; text-align: center;">
Drop .OSR file here
</div>
<div id="output"></div>
<script>
const dropArea = document.getElementById('drop-area');
const output = document.getElementById('output');
dropArea.addEventListener('dragover', (e) => { e.preventDefault(); dropArea.style.borderColor = '#000'; });
dropArea.addEventListener('dragleave', () => { dropArea.style.borderColor = '#ccc'; });
dropArea.addEventListener('drop', (e) => {
e.preventDefault(); dropArea.style.borderColor = '#ccc';
const file = e.dataTransfer.files[0];
if (file.name.endsWith('.osr')) parseOSR(file);
else output.innerHTML = 'Invalid file type.';
});
function parseOSR(file) {
const reader = new FileReader();
reader.onload = (e) => {
const buffer = e.target.result;
const view = new DataView(buffer);
let offset = 0;
const properties = {};
properties.gameMode = view.getUint8(offset); offset += 1;
properties.gameVersion = view.getInt32(offset, true); offset += 4;
properties.beatmapMD5 = readString(view, offset); offset = properties.beatmapMD5.nextOffset;
properties.playerName = readString(view, offset); offset = properties.playerName.nextOffset;
properties.replayMD5 = readString(view, offset); offset = properties.replayMD5.nextOffset;
properties.num300s = view.getUint16(offset, true); offset += 2;
properties.num100s = view.getUint16(offset, true); offset += 2;
properties.num50s = view.getUint16(offset, true); offset += 2;
properties.numGekis = view.getUint16(offset, true); offset += 2;
properties.numKatus = view.getUint16(offset, true); offset += 2;
properties.numMisses = view.getUint16(offset, true); offset += 2;
properties.totalScore = view.getInt32(offset, true); offset += 4;
properties.maxCombo = view.getUint16(offset, true); offset += 2;
properties.perfect = view.getUint8(offset); offset += 1;
properties.mods = view.getInt32(offset, true); offset += 4;
properties.lifeBarGraph = readString(view, offset); offset = properties.lifeBarGraph.nextOffset;
properties.timestamp = Number(view.getBigInt64(offset, true)); offset += 8;
properties.compressedLength = view.getInt32(offset, true); offset += 4;
properties.compressedData = buffer.slice(offset, offset + properties.compressedLength); offset += properties.compressedLength;
properties.onlineScoreID = Number(view.getBigInt64(offset, true)); offset += 8;
// Optional double if Target Practice mod (bit 23 set)
if (properties.mods & (1 << 23)) {
properties.additionalModInfo = view.getFloat64(offset, true); offset += 8;
}
let html = '<h3>.OSR Properties:</h3><ul>';
for (const [key, value] of Object.entries(properties)) {
html += `<li><strong>${key}:</strong> ${typeof value === 'object' ? 'Binary data' : value}</li>`;
}
html += '</ul>';
output.innerHTML = html;
};
reader.readAsArrayBuffer(file);
}
function readString(view, offset) {
if (view.getUint8(offset) === 0x00) return { value: '', nextOffset: offset + 1 };
offset += 1;
let length = 0, shift = 0;
while (true) {
const byte = view.getUint8(offset++);
length += (byte & 0x7F) << shift;
if ((byte & 0x80) === 0) break;
shift += 7;
}
const value = new TextDecoder('utf-8').decode(new Uint8Array(view.buffer, offset, length));
return { value, nextOffset: offset + length };
}
</script>
- Python class for .OSR handling:
import struct
import lzma
from io import BytesIO
class OSRFile:
def __init__(self, filepath=None):
self.properties = {}
if filepath:
self.read(filepath)
def read(self, filepath):
with open(filepath, 'rb') as f:
data = f.read()
view = BytesIO(data)
self.properties['gameMode'] = struct.unpack('<B', view.read(1))[0]
self.properties['gameVersion'] = struct.unpack('<i', view.read(4))[0]
self.properties['beatmapMD5'] = self._read_string(view)
self.properties['playerName'] = self._read_string(view)
self.properties['replayMD5'] = self._read_string(view)
self.properties['num300s'] = struct.unpack('<H', view.read(2))[0]
self.properties['num100s'] = struct.unpack('<H', view.read(2))[0]
self.properties['num50s'] = struct.unpack('<H', view.read(2))[0]
self.properties['numGekis'] = struct.unpack('<H', view.read(2))[0]
self.properties['numKatus'] = struct.unpack('<H', view.read(2))[0]
self.properties['numMisses'] = struct.unpack('<H', view.read(2))[0]
self.properties['totalScore'] = struct.unpack('<i', view.read(4))[0]
self.properties['maxCombo'] = struct.unpack('<H', view.read(2))[0]
self.properties['perfect'] = struct.unpack('<B', view.read(1))[0]
self.properties['mods'] = struct.unpack('<i', view.read(4))[0]
self.properties['lifeBarGraph'] = self._read_string(view)
self.properties['timestamp'] = struct.unpack('<q', view.read(8))[0]
compressed_length = struct.unpack('<i', view.read(4))[0]
self.properties['compressedData'] = view.read(compressed_length)
self.properties['onlineScoreID'] = struct.unpack('<q', view.read(8))[0]
if self.properties['mods'] & (1 << 23):
self.properties['additionalModInfo'] = struct.unpack('<d', view.read(8))[0]
def _read_string(self, view):
flag = struct.unpack('<B', view.read(1))[0]
if flag == 0x00:
return ''
elif flag == 0x0b:
length = 0
shift = 0
while True:
byte = struct.unpack('<B', view.read(1))[0]
length += (byte & 0x7f) << shift
shift += 7
if not (byte & 0x80):
break
return view.read(length).decode('utf-8')
return ''
def print_properties(self):
for key, value in self.properties.items():
if key == 'compressedData':
print(f"{key}: Binary data (length {len(value)})")
else:
print(f"{key}: {value}")
def write(self, filepath):
with open(filepath, 'wb') as f:
f.write(struct.pack('<B', self.properties.get('gameMode', 0)))
f.write(struct.pack('<i', self.properties.get('gameVersion', 0)))
self._write_string(f, self.properties.get('beatmapMD5', ''))
self._write_string(f, self.properties.get('playerName', ''))
self._write_string(f, self.properties.get('replayMD5', ''))
f.write(struct.pack('<H', self.properties.get('num300s', 0)))
f.write(struct.pack('<H', self.properties.get('num100s', 0)))
f.write(struct.pack('<H', self.properties.get('num50s', 0)))
f.write(struct.pack('<H', self.properties.get('numGekis', 0)))
f.write(struct.pack('<H', self.properties.get('numKatus', 0)))
f.write(struct.pack('<H', self.properties.get('numMisses', 0)))
f.write(struct.pack('<i', self.properties.get('totalScore', 0)))
f.write(struct.pack('<H', self.properties.get('maxCombo', 0)))
f.write(struct.pack('<B', self.properties.get('perfect', 0)))
f.write(struct.pack('<i', self.properties.get('mods', 0)))
self._write_string(f, self.properties.get('lifeBarGraph', ''))
f.write(struct.pack('<q', self.properties.get('timestamp', 0)))
compressed_data = self.properties.get('compressedData', b'')
f.write(struct.pack('<i', len(compressed_data)))
f.write(compressed_data)
f.write(struct.pack('<q', self.properties.get('onlineScoreID', 0)))
if self.properties.get('mods', 0) & (1 << 23):
f.write(struct.pack('<d', self.properties.get('additionalModInfo', 0.0)))
def _write_string(self, f, s):
if not s:
f.write(struct.pack('<B', 0x00))
return
f.write(struct.pack('<B', 0x0b))
length = len(s.encode('utf-8'))
while length > 0:
byte = length & 0x7f
length >>= 7
if length > 0:
byte |= 0x80
f.write(struct.pack('<B', byte))
f.write(s.encode('utf-8'))
- Java class for .OSR handling:
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class OSRFile {
private byte gameMode;
private int gameVersion;
private String beatmapMD5;
private String playerName;
private String replayMD5;
private short num300s;
private short num100s;
private short num50s;
private short numGekis;
private short numKatus;
private short numMisses;
private int totalScore;
private short maxCombo;
private byte perfect;
private int mods;
private String lifeBarGraph;
private long timestamp;
private int compressedLength;
private byte[] compressedData;
private long onlineScoreID;
private double additionalModInfo; // Optional
public OSRFile(String filepath) throws IOException {
read(filepath);
}
public OSRFile() {}
private String readString(DataInputStream dis) throws IOException {
byte flag = dis.readByte();
if (flag == 0x00) return "";
if (flag != 0x0b) throw new IOException("Invalid string flag");
int length = 0;
int shift = 0;
while (true) {
byte byteVal = dis.readByte();
length += (byteVal & 0x7F) << shift;
if ((byteVal & 0x80) == 0) break;
shift += 7;
}
byte[] bytes = new byte[length];
dis.readFully(bytes);
return new String(bytes, "UTF-8");
}
private void writeString(DataOutputStream dos, String s) throws IOException {
if (s.isEmpty()) {
dos.writeByte(0x00);
return;
}
dos.writeByte(0x0b);
byte[] bytes = s.getBytes("UTF-8");
int length = bytes.length;
while (length > 0) {
int byteVal = length & 0x7f;
length >>= 7;
if (length > 0) byteVal |= 0x80;
dos.writeByte(byteVal);
}
dos.write(bytes);
}
public void read(String filepath) throws IOException {
try (FileInputStream fis = new FileInputStream(filepath);
DataInputStream dis = new DataInputStream(fis)) {
gameMode = dis.readByte();
gameVersion = Integer.reverseBytes(dis.readInt());
beatmapMD5 = readString(dis);
playerName = readString(dis);
replayMD5 = readString(dis);
num300s = Short.reverseBytes(dis.readShort());
num100s = Short.reverseBytes(dis.readShort());
num50s = Short.reverseBytes(dis.readShort());
numGekis = Short.reverseBytes(dis.readShort());
numKatus = Short.reverseBytes(dis.readShort());
numMisses = Short.reverseBytes(dis.readShort());
totalScore = Integer.reverseBytes(dis.readInt());
maxCombo = Short.reverseBytes(dis.readShort());
perfect = dis.readByte();
mods = Integer.reverseBytes(dis.readInt());
lifeBarGraph = readString(dis);
timestamp = Long.reverseBytes(dis.readLong());
compressedLength = Integer.reverseBytes(dis.readInt());
compressedData = new byte[compressedLength];
dis.readFully(compressedData);
onlineScoreID = Long.reverseBytes(dis.readLong());
if ((mods & (1 << 23)) != 0) {
ByteBuffer bb = ByteBuffer.allocate(8);
bb.order(ByteOrder.LITTLE_ENDIAN);
bb.putDouble(dis.readDouble());
additionalModInfo = bb.getDouble(0);
}
}
}
public void printProperties() {
System.out.println("gameMode: " + gameMode);
System.out.println("gameVersion: " + gameVersion);
System.out.println("beatmapMD5: " + beatmapMD5);
System.out.println("playerName: " + playerName);
System.out.println("replayMD5: " + replayMD5);
System.out.println("num300s: " + num300s);
System.out.println("num100s: " + num100s);
System.out.println("num50s: " + num50s);
System.out.println("numGekis: " + numGekis);
System.out.println("numKatus: " + numKatus);
System.out.println("numMisses: " + numMisses);
System.out.println("totalScore: " + totalScore);
System.out.println("maxCombo: " + maxCombo);
System.out.println("perfect: " + perfect);
System.out.println("mods: " + mods);
System.out.println("lifeBarGraph: " + lifeBarGraph);
System.out.println("timestamp: " + timestamp);
System.out.println("compressedLength: " + compressedLength);
System.out.println("compressedData: Binary data (length " + compressedLength + ")");
System.out.println("onlineScoreID: " + onlineScoreID);
if ((mods & (1 << 23)) != 0) {
System.out.println("additionalModInfo: " + additionalModInfo);
}
}
public void write(String filepath) throws IOException {
try (FileOutputStream fos = new FileOutputStream(filepath);
DataOutputStream dos = new DataOutputStream(fos)) {
dos.writeByte(gameMode);
dos.writeInt(Integer.reverseBytes(gameVersion));
writeString(dos, beatmapMD5);
writeString(dos, playerName);
writeString(dos, replayMD5);
dos.writeShort(Short.reverseBytes(num300s));
dos.writeShort(Short.reverseBytes(num100s));
dos.writeShort(Short.reverseBytes(num50s));
dos.writeShort(Short.reverseBytes(numGekis));
dos.writeShort(Short.reverseBytes(numKatus));
dos.writeShort(Short.reverseBytes(numMisses));
dos.writeInt(Integer.reverseBytes(totalScore));
dos.writeShort(Short.reverseBytes(maxCombo));
dos.writeByte(perfect);
dos.writeInt(Integer.reverseBytes(mods));
writeString(dos, lifeBarGraph);
dos.writeLong(Long.reverseBytes(timestamp));
dos.writeInt(Integer.reverseBytes(compressedLength));
dos.write(compressedData);
dos.writeLong(Long.reverseBytes(onlineScoreID));
if ((mods & (1 << 23)) != 0) {
dos.writeDouble(Double.longBitsToDouble(Long.reverseBytes(Double.doubleToLongBits(additionalModInfo))));
}
}
}
}
- JavaScript class for .OSR handling:
class OSRFile {
constructor(filepath = null) {
this.properties = {};
if (filepath) this.read(filepath);
}
async read(filepath) {
const buffer = await fetch(filepath).then(res => res.arrayBuffer());
const view = new DataView(buffer);
let offset = 0;
this.properties.gameMode = view.getUint8(offset); offset += 1;
this.properties.gameVersion = view.getInt32(offset, true); offset += 4;
const beatmapRes = this._readString(view, offset); this.properties.beatmapMD5 = beatmapRes.value; offset = beatmapRes.nextOffset;
const playerRes = this._readString(view, offset); this.properties.playerName = playerRes.value; offset = playerRes.nextOffset;
const replayRes = this._readString(view, offset); this.properties.replayMD5 = replayRes.value; offset = replayRes.nextOffset;
this.properties.num300s = view.getUint16(offset, true); offset += 2;
this.properties.num100s = view.getUint16(offset, true); offset += 2;
this.properties.num50s = view.getUint16(offset, true); offset += 2;
this.properties.numGekis = view.getUint16(offset, true); offset += 2;
this.properties.numKatus = view.getUint16(offset, true); offset += 2;
this.properties.numMisses = view.getUint16(offset, true); offset += 2;
this.properties.totalScore = view.getInt32(offset, true); offset += 4;
this.properties.maxCombo = view.getUint16(offset, true); offset += 2;
this.properties.perfect = view.getUint8(offset); offset += 1;
this.properties.mods = view.getInt32(offset, true); offset += 4;
const lifeRes = this._readString(view, offset); this.properties.lifeBarGraph = lifeRes.value; offset = lifeRes.nextOffset;
this.properties.timestamp = Number(view.getBigInt64(offset, true)); offset += 8;
this.properties.compressedLength = view.getInt32(offset, true); offset += 4;
this.properties.compressedData = buffer.slice(offset, offset + this.properties.compressedLength); offset += this.properties.compressedLength;
this.properties.onlineScoreID = Number(view.getBigInt64(offset, true)); offset += 8;
if (this.properties.mods & (1 << 23)) {
this.properties.additionalModInfo = view.getFloat64(offset, true); offset += 8;
}
}
_readString(view, offset) {
const flag = view.getUint8(offset++);
if (flag === 0x00) return { value: '', nextOffset: offset };
if (flag !== 0x0b) throw new Error('Invalid string flag');
let length = 0, shift = 0;
while (true) {
const byte = view.getUint8(offset++);
length += (byte & 0x7f) << shift;
if ((byte & 0x80) === 0) break;
shift += 7;
}
const decoder = new TextDecoder('utf-8');
const value = decoder.decode(new Uint8Array(view.buffer, offset, length));
return { value, nextOffset: offset + length };
}
printProperties() {
for (const [key, value] of Object.entries(this.properties)) {
console.log(`${key}: ${key === 'compressedData' ? `Binary data (length ${value.byteLength})` : value}`);
}
}
write(filepath) {
// Writing in JS typically requires Node.js or blob download; here's a blob example for browser
const buffer = new ArrayBuffer(1024 * 1024); // Estimate size
const view = new DataView(buffer);
let offset = 0;
view.setUint8(offset, this.properties.gameMode || 0); offset += 1;
view.setInt32(offset, this.properties.gameVersion || 0, true); offset += 4;
offset = this._writeString(view, offset, this.properties.beatmapMD5 || '');
offset = this._writeString(view, offset, this.properties.playerName || '');
offset = this._writeString(view, offset, this.properties.replayMD5 || '');
view.setUint16(offset, this.properties.num300s || 0, true); offset += 2;
view.setUint16(offset, this.properties.num100s || 0, true); offset += 2;
view.setUint16(offset, this.properties.num50s || 0, true); offset += 2;
view.setUint16(offset, this.properties.numGekis || 0, true); offset += 2;
view.setUint16(offset, this.properties.numKatus || 0, true); offset += 2;
view.setUint16(offset, this.properties.numMisses || 0, true); offset += 2;
view.setInt32(offset, this.properties.totalScore || 0, true); offset += 4;
view.setUint16(offset, this.properties.maxCombo || 0, true); offset += 2;
view.setUint8(offset, this.properties.perfect || 0); offset += 1;
view.setInt32(offset, this.properties.mods || 0, true); offset += 4;
offset = this._writeString(view, offset, this.properties.lifeBarGraph || '');
view.setBigInt64(offset, BigInt(this.properties.timestamp || 0), true); offset += 8;
const compressedData = this.properties.compressedData || new ArrayBuffer(0);
view.setInt32(offset, compressedData.byteLength, true); offset += 4;
new Uint8Array(buffer, offset, compressedData.byteLength).set(new Uint8Array(compressedData)); offset += compressedData.byteLength;
view.setBigInt64(offset, BigInt(this.properties.onlineScoreID || 0), true); offset += 8;
if (this.properties.mods & (1 << 23)) {
view.setFloat64(offset, this.properties.additionalModInfo || 0, true); offset += 8;
}
const blob = new Blob([buffer.slice(0, offset)]);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filepath || 'output.osr';
a.click();
URL.revokeObjectURL(url);
}
_writeString(view, offset, s) {
if (!s) {
view.setUint8(offset, 0x00);
return offset + 1;
}
view.setUint8(offset, 0x0b); offset += 1;
const encoder = new TextEncoder();
const bytes = encoder.encode(s);
let length = bytes.length;
while (length > 0) {
let byte = length & 0x7f;
length >>= 7;
if (length > 0) byte |= 0x80;
view.setUint8(offset, byte); offset += 1;
}
new Uint8Array(view.buffer, offset, bytes.length).set(bytes); offset += bytes.length;
return offset;
}
}
- C class (using C++ for class support):
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
#include <cstdint>
#include <endian.h> // For little-endian conversions if needed
class OSRFile {
private:
uint8_t gameMode;
int32_t gameVersion;
std::string beatmapMD5;
std::string playerName;
std::string replayMD5;
uint16_t num300s;
uint16_t num100s;
uint16_t num50s;
uint16_t numGekis;
uint16_t numKatus;
uint16_t numMisses;
int32_t totalScore;
uint16_t maxCombo;
uint8_t perfect;
int32_t mods;
std::string lifeBarGraph;
int64_t timestamp;
int32_t compressedLength;
std::vector<uint8_t> compressedData;
int64_t onlineScoreID;
double additionalModInfo; // Optional
std::string readString(std::ifstream& ifs) {
uint8_t flag;
ifs.read(reinterpret_cast<char*>(&flag), 1);
if (flag == 0x00) return "";
if (flag != 0x0b) throw std::runtime_error("Invalid string flag");
uint32_t length = 0;
uint32_t shift = 0;
while (true) {
uint8_t byte;
ifs.read(reinterpret_cast<char*>(&byte), 1);
length += (byte & 0x7f) << shift;
if ((byte & 0x80) == 0) break;
shift += 7;
}
std::string s(length, '\0');
ifs.read(&s[0], length);
return s;
}
void writeString(std::ofstream& ofs, const std::string& s) {
if (s.empty()) {
ofs.put(0x00);
return;
}
ofs.put(0x0b);
uint32_t length = s.size();
while (length > 0) {
uint8_t byte = length & 0x7f;
length >>= 7;
if (length > 0) byte |= 0x80;
ofs.put(byte);
}
ofs.write(s.data(), s.size());
}
public:
OSRFile(const std::string& filepath = "") {
if (!filepath.empty()) read(filepath);
}
void read(const std::string& filepath) {
std::ifstream ifs(filepath, std::ios::binary);
if (!ifs) throw std::runtime_error("Cannot open file");
ifs.read(reinterpret_cast<char*>(&gameMode), 1);
ifs.read(reinterpret_cast<char*>(&gameVersion), 4); gameVersion = le32toh(gameVersion);
beatmapMD5 = readString(ifs);
playerName = readString(ifs);
replayMD5 = readString(ifs);
ifs.read(reinterpret_cast<char*>(&num300s), 2); num300s = le16toh(num300s);
ifs.read(reinterpret_cast<char*>(&num100s), 2); num100s = le16toh(num100s);
ifs.read(reinterpret_cast<char*>(&num50s), 2); num50s = le16toh(num50s);
ifs.read(reinterpret_cast<char*>(&numGekis), 2); numGekis = le16toh(numGekis);
ifs.read(reinterpret_cast<char*>(&numKatus), 2); numKatus = le16toh(numKatus);
ifs.read(reinterpret_cast<char*>(&numMisses), 2); numMisses = le16toh(numMisses);
ifs.read(reinterpret_cast<char*>(&totalScore), 4); totalScore = le32toh(totalScore);
ifs.read(reinterpret_cast<char*>(&maxCombo), 2); maxCombo = le16toh(maxCombo);
ifs.read(reinterpret_cast<char*>(&perfect), 1);
ifs.read(reinterpret_cast<char*>(&mods), 4); mods = le32toh(mods);
lifeBarGraph = readString(ifs);
ifs.read(reinterpret_cast<char*>(×tamp), 8); timestamp = le64toh(timestamp);
ifs.read(reinterpret_cast<char*>(&compressedLength), 4); compressedLength = le32toh(compressedLength);
compressedData.resize(compressedLength);
ifs.read(reinterpret_cast<char*>(compressedData.data()), compressedLength);
ifs.read(reinterpret_cast<char*>(&onlineScoreID), 8); onlineScoreID = le64toh(onlineScoreID);
if (mods & (1 << 23)) {
ifs.read(reinterpret_cast<char*>(&additionalModInfo), 8);
uint64_t bits = le64toh(*reinterpret_cast<uint64_t*>(&additionalModInfo));
additionalModInfo = *reinterpret_cast<double*>(&bits);
}
}
void printProperties() const {
std::cout << "gameMode: " << static_cast<int>(gameMode) << std::endl;
std::cout << "gameVersion: " << gameVersion << std::endl;
std::cout << "beatmapMD5: " << beatmapMD5 << std::endl;
std::cout << "playerName: " << playerName << std::endl;
std::cout << "replayMD5: " << replayMD5 << std::endl;
std::cout << "num300s: " << num300s << std::endl;
std::cout << "num100s: " << num100s << std::endl;
std::cout << "num50s: " << num50s << std::endl;
std::cout << "numGekis: " << numGekis << std::endl;
std::cout << "numKatus: " << numKatus << std::endl;
std::cout << "numMisses: " << numMisses << std::endl;
std::cout << "totalScore: " << totalScore << std::endl;
std::cout << "maxCombo: " << maxCombo << std::endl;
std::cout << "perfect: " << static_cast<int>(perfect) << std::endl;
std::cout << "mods: " << mods << std::endl;
std::cout << "lifeBarGraph: " << lifeBarGraph << std::endl;
std::cout << "timestamp: " << timestamp << std::endl;
std::cout << "compressedLength: " << compressedLength << std::endl;
std::cout << "compressedData: Binary data (length " << compressedData.size() << ")" << std::endl;
std::cout << "onlineScoreID: " << onlineScoreID << std::endl;
if (mods & (1 << 23)) {
std::cout << "additionalModInfo: " << additionalModInfo << std::endl;
}
}
void write(const std::string& filepath) {
std::ofstream ofs(filepath, std::ios::binary);
if (!ofs) throw std::runtime_error("Cannot open file");
ofs.write(reinterpret_cast<const char*>(&gameMode), 1);
int32_t leGameVersion = htole32(gameVersion);
ofs.write(reinterpret_cast<const char*>(&leGameVersion), 4);
writeString(ofs, beatmapMD5);
writeString(ofs, playerName);
writeString(ofs, replayMD5);
uint16_t leNum300s = htole16(num300s);
ofs.write(reinterpret_cast<const char*>(&leNum300s), 2);
uint16_t leNum100s = htole16(num100s);
ofs.write(reinterpret_cast<const char*>(&leNum100s), 2);
uint16_t leNum50s = htole16(num50s);
ofs.write(reinterpret_cast<const char*>(&leNum50s), 2);
uint16_t leNumGekis = htole16(numGekis);
ofs.write(reinterpret_cast<const char*>(&leNumGekis), 2);
uint16_t leNumKatus = htole16(numKatus);
ofs.write(reinterpret_cast<const char*>(&leNumKatus), 2);
uint16_t leNumMisses = htole16(numMisses);
ofs.write(reinterpret_cast<const char*>(&leNumMisses), 2);
int32_t leTotalScore = htole32(totalScore);
ofs.write(reinterpret_cast<const char*>(&leTotalScore), 4);
uint16_t leMaxCombo = htole16(maxCombo);
ofs.write(reinterpret_cast<const char*>(&leMaxCombo), 2);
ofs.write(reinterpret_cast<const char*>(&perfect), 1);
int32_t leMods = htole32(mods);
ofs.write(reinterpret_cast<const char*>(&leMods), 4);
writeString(ofs, lifeBarGraph);
int64_t leTimestamp = htole64(timestamp);
ofs.write(reinterpret_cast<const char*>(&leTimestamp), 8);
int32_t leCompressedLength = htole32(compressedData.size());
ofs.write(reinterpret_cast<const char*>(&leCompressedLength), 4);
ofs.write(reinterpret_cast<const char*>(compressedData.data()), compressedData.size());
int64_t leOnlineScoreID = htole64(onlineScoreID);
ofs.write(reinterpret_cast<const char*>(&leOnlineScoreID), 8);
if (mods & (1 << 23)) {
uint64_t bits = htole64(*reinterpret_cast<uint64_t*>(&additionalModInfo));
ofs.write(reinterpret_cast<const char*>(&bits), 8);
}
}
};