mod actions;
#[cfg(any(feature = "replay-data", feature = "replay-data-xz2"))]
mod lzma;
use std::io::{Read, Write};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use crate::data::{Mode, Mods};
use crate::db::{ReadBytesOsu, WriteBytesOsu};
pub use self::actions::{Buttons, ReplayAction, ReplayActionData};
pub type ReplayResult<T, E = ReplayError> = std::result::Result<T, E>;
#[allow(missing_docs)]
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum ReplayError {
#[cfg(any(feature = "replay-data-xz2"))]
#[cfg_attr(docsrs, doc(cfg(feature = "replay-data-xz2")))]
#[error("error creating lzma decoder: {0}")]
LzmaCreate(#[from] xz2::stream::Error),
#[cfg(any(feature = "replay-data"))]
#[cfg_attr(docsrs, doc(cfg(feature = "replay-data")))]
#[error("error creating lzma decoder: {0}")]
LzmaCreate(#[from] lzma_rs::error::Error),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("error decoding utf8: {0}")]
Utf8(#[from] std::string::FromUtf8Error),
#[error("error parsing int: {0}")]
ParseInt(#[from] std::num::ParseIntError),
#[error("error parsing float: {0}")]
ParseFloat(#[from] std::num::ParseFloatError),
#[error("binary data error: {0}")]
Binary(#[from] crate::db::binary::Error),
#[error("missing field in life graph")]
LifeGraphMissing,
#[error("unexpected mods: {0}")]
UnexpectedMods(u32),
#[error("invalid mode: {0}")]
InvalidMode(u8),
#[error("invalid buttons: {0}")]
InvalidButtons(u32),
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Replay {
pub mode: Mode,
pub version: u32,
pub beatmap_hash: String,
pub player_username: String,
pub replay_hash: String,
pub count_300: u16,
pub count_100: u16,
pub count_50: u16,
pub count_geki: u16,
pub count_katu: u16,
pub count_miss: u16,
pub score: u32,
pub max_combo: u16,
pub perfect: bool,
pub mods: Mods,
pub life_graph: Vec<(i32, f64)>,
pub timestamp: u64,
pub action_data: Vec<u8>,
pub score_id: Option<u64>,
pub target_practice_total_accuracy: Option<f64>,
}
impl Replay {
pub fn parse<R: Read>(reader: &mut R) -> ReplayResult<Replay> {
let mode = match reader.read_u8()? {
0 => Mode::Osu,
1 => Mode::Taiko,
2 => Mode::Catch,
3 => Mode::Mania,
x => return Err(ReplayError::InvalidMode(x)),
};
let version = reader.read_u32::<LittleEndian>()?;
let beatmap_hash = reader.read_uleb128_string()?;
let player_username = reader.read_uleb128_string()?;
let replay_hash = reader.read_uleb128_string()?;
let count_300 = reader.read_u16::<LittleEndian>()?;
let count_100 = reader.read_u16::<LittleEndian>()?;
let count_50 = reader.read_u16::<LittleEndian>()?;
let count_geki = reader.read_u16::<LittleEndian>()?;
let count_katu = reader.read_u16::<LittleEndian>()?;
let count_miss = reader.read_u16::<LittleEndian>()?;
let score = reader.read_u32::<LittleEndian>()?;
let max_combo = reader.read_u16::<LittleEndian>()?;
let perfect = reader.read_u8()? == 1;
let mods_value = reader.read_u32::<LittleEndian>()?;
let mods = Mods::from_bits(mods_value)
.ok_or(ReplayError::UnexpectedMods(mods_value))?;
let life_graph = reader
.read_uleb128_string()?
.split(',')
.filter_map(|frame| {
if frame.is_empty() {
None
} else {
Some(frame.split('|'))
}
})
.map(|mut frame| {
Ok((
frame
.next()
.ok_or(ReplayError::LifeGraphMissing)?
.parse::<i32>()?,
frame
.next()
.ok_or(ReplayError::LifeGraphMissing)?
.parse::<f64>()?,
))
})
.collect::<ReplayResult<Vec<_>>>()?;
let timestamp = reader.read_u64::<LittleEndian>()?;
let replay_data_length = reader.read_u32::<LittleEndian>()?;
let mut action_data = vec![0; replay_data_length as usize];
reader.read_exact(&mut action_data)?;
let score_id = match reader.read_u64::<LittleEndian>()? {
0 => None,
v => Some(v),
};
let target_practice_total_accuracy = if mods.contains(Mods::TargetPractice)
{
Some(reader.read_f64::<LittleEndian>()?)
} else {
None
};
Ok(Replay {
mode,
version,
beatmap_hash,
player_username,
replay_hash,
count_300,
count_100,
count_50,
count_geki,
count_katu,
count_miss,
score,
max_combo,
perfect,
mods,
life_graph,
timestamp,
action_data,
score_id,
target_practice_total_accuracy,
})
}
pub fn write<W: Write>(&self, mut w: W) -> ReplayResult<()> {
w.write_u8(self.mode as u8)?;
w.write_u32::<LittleEndian>(self.version)?;
w.write_uleb128_string(&self.beatmap_hash)?;
w.write_uleb128_string(&self.player_username)?;
w.write_uleb128_string(&self.replay_hash)?;
w.write_u16::<LittleEndian>(self.count_300)?;
w.write_u16::<LittleEndian>(self.count_100)?;
w.write_u16::<LittleEndian>(self.count_50)?;
w.write_u16::<LittleEndian>(self.count_geki)?;
w.write_u16::<LittleEndian>(self.count_katu)?;
w.write_u16::<LittleEndian>(self.count_miss)?;
w.write_u32::<LittleEndian>(self.score)?;
w.write_u16::<LittleEndian>(self.max_combo)?;
w.write_u8(if self.perfect { 0 } else { 1 })?;
w.write_u32::<LittleEndian>(self.mods.bits())?;
w.write_uleb128_string(
&self
.life_graph
.iter()
.map(|(time, life)| format!("{}|{}", time, life))
.collect::<Vec<_>>()
.join(","),
)?;
w.write_u64::<LittleEndian>(self.timestamp)?;
w.write_u32::<LittleEndian>(self.action_data.len() as u32)?;
w.write_all(&self.action_data)?;
w.write_u64::<LittleEndian>(self.score_id.unwrap_or(0))?;
if let Some(acc) = self.target_practice_total_accuracy {
w.write_f64::<LittleEndian>(acc)?;
}
Ok(())
}
#[cfg(feature = "replay-data")]
#[cfg_attr(docsrs, doc(cfg(feature = "replay-data")))]
pub fn update_action_data(
&mut self,
action_data: &ReplayActionData,
) -> ReplayResult<()> {
{
let mut writer = Vec::new();
for (i, frame) in action_data.frames.iter().enumerate() {
if i > 0 {
writer.write_all(&[b','])?;
}
let this_frame = format!(
"{}|{}|{}|{}",
frame.time.0,
frame.x,
frame.y,
frame.buttons.bits()
);
writer.write_all(this_frame.as_bytes())?;
}
if let Some(seed) = action_data.rng_seed {
let this_frame = format!(",-12345|0|0|{}", seed);
writer.write_all(this_frame.as_bytes())?;
}
self.action_data = lzma::encode(writer.as_slice())?;
}
Ok(())
}
#[cfg(feature = "replay-data")]
#[cfg_attr(docsrs, doc(cfg(feature = "replay-data")))]
pub fn parse_action_data(&self) -> ReplayResult<ReplayActionData> {
use std::io::Cursor;
let cursor = Cursor::new(&self.action_data);
ReplayActionData::parse(cursor)
}
}