rosu-replay

Crates.iorosu-replay
lib.rsrosu-replay
version0.2.1
created_at2025-08-31 17:07:53.279918+00
updated_at2025-09-14 21:43:47.921396+00
descriptionA Rust library for parsing and writing osu! replay files (.osr format), ported from the Python osrparse library
homepage
repositoryhttps://github.com/Glubus/rosu-replay
max_upload_size
id1818703
size130,967
Osef (Glubus)

documentation

https://docs.rs/rosu-replay

README

rosu-replay

Crates.io Documentation License Build Status

A high-performance Rust library for parsing and writing osu! replay files (.osr format), with WebAssembly support.

This library is a faithful port of the Python osrparse library, providing the same functionality for parsing and manipulating osu! replay files in Rust with improved performance, memory safety, and additional features.

โœจ Features

  • ๐ŸŽฎ Parse .osr replay files from disk, memory, or web
  • ๐Ÿ“Š Extract complete replay metadata including:
    • Player information (username, score, combo, etc.)
    • Game metadata (mode, mods, timestamp, etc.)
    • Hit statistics (300s, 100s, 50s, misses, etc.)
    • Replay events (cursor movement, key presses)
    • Life bar data over time
  • ๐Ÿ’พ Write replay files back to .osr format (compressed and uncompressed)
  • ๐ŸŽฏ Support all game modes: osu!standard, osu!taiko, osu!catch, osu!mania
  • ๐ŸŒ API compatibility for parsing replay data from osu! API v1 responses
  • ๐Ÿ•ธ๏ธ WebAssembly support for browser and Node.js environments
  • โšก High performance with optimized LZMA compression via liblzma
  • ๐Ÿฆ€ Memory safe Rust implementation with zero-copy parsing where possible
  • ๐Ÿ“– Comprehensive documentation and examples
  • ๐Ÿงช Extensive testing with 40+ tests covering all functionality

๐Ÿš€ Installation

Rust/Cargo

Add this to your Cargo.toml:

[dependencies]
rosu-replay = "0.1"

WebAssembly

For WASM usage, enable the wasm feature:

[dependencies]
rosu-replay = { version = "0.1", features = ["wasm"] }

Then compile with:

wasm-pack build --features wasm --target web

๐Ÿ“– Quick Start

Basic Replay Parsing

use rosu_replay::Replay;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Parse a replay file
    let replay = Replay::from_path("path/to/replay.osr")?;
    
    // Access basic information
    println!("Player: {}", replay.username);
    println!("Score: {}", replay.score);
    println!("Max Combo: {}", replay.max_combo);
    println!("Game Mode: {:?}", replay.mode);
    println!("Mods: {:?}", replay.mods);
    
    // Access hit statistics
    println!("300s: {}, 100s: {}, 50s: {}, Misses: {}", 
        replay.count_300, replay.count_100, replay.count_50, replay.count_miss);
    
    // Check if it's a perfect play
    println!("Perfect: {}", replay.count_miss == 0);
    
    Ok(())
}

Working with Replay Events

use rosu_replay::{Replay, ReplayEvent};

let replay = Replay::from_path("replay.osr")?;

// Iterate through replay events
for (i, event) in replay.replay_data.iter().enumerate().take(10) {
    match event {
        ReplayEvent::Osu(osu_event) => {
            println!("Frame {}: Cursor at ({:.1}, {:.1}) at +{}ms, keys: {}", 
                i, osu_event.x, osu_event.y, osu_event.time_delta, osu_event.keys.value());
        }
        ReplayEvent::Taiko(taiko_event) => {
            println!("Frame {}: Taiko input at +{}ms, keys: {}", 
                i, taiko_event.time_delta, taiko_event.keys.value());
        }
        ReplayEvent::Catch(catch_event) => {
            println!("Frame {}: Catch at x={:.1}, +{}ms, dashing: {}", 
                i, catch_event.x, catch_event.time_delta, catch_event.dashing);
        }
        ReplayEvent::Mania(mania_event) => {
            println!("Frame {}: Mania keys {} at +{}ms", 
                i, mania_event.keys.value(), mania_event.time_delta);
        }
    }
}

Modifying and Writing Replays

use rosu_replay::{Replay, Packer};

let mut replay = Replay::from_path("input.osr")?;

// Modify replay data
replay.username = "Modified Player".to_string();
replay.score = 1000000;

// Write compressed (default)
replay.write_path("modified_replay.osr")?;

// Write uncompressed for faster loading
replay.write_path_uncompressed("uncompressed_replay.osr")?;

// Custom compression settings
let custom_packer = Packer::new().with_preset(9); // Maximum compression
let bytes = replay.pack_with(&custom_packer)?;
std::fs::write("custom_compressed.osr", bytes)?;

Working with API Data

use rosu_replay::{parse_replay_data, GameMode};

// Parse replay data from osu! API v1
let api_data = b"base64_encoded_replay_data_from_api";
let events = parse_replay_data(api_data, false, false, GameMode::Std)?;

for event in events.iter().take(5) {
    if let rosu_replay::ReplayEvent::Osu(osu_event) = event {
        println!("API Event: ({:.1}, {:.1}) +{}ms", 
            osu_event.x, osu_event.y, osu_event.time_delta);
    }
}

๐Ÿ•ธ๏ธ WebAssembly Usage

Browser JavaScript

import init, { WasmReplay, WasmGameMode, parse_replay_data_wasm } from './pkg/rosu_replay.js';

async function parseReplay() {
    await init();
    
    // Load replay file (from file input, fetch, etc.)
    const replayBytes = new Uint8Array(await file.arrayBuffer());
    
    // Parse replay
    const replay = new WasmReplay(replayBytes);
    
    console.log(`Player: ${replay.username}`);
    console.log(`Score: ${replay.score}`);
    console.log(`Mode: ${replay.mode}`);
    console.log(`Events: ${replay.event_count}`);
    
    // Pack back to bytes
    const packedBytes = replay.pack();
}

Node.js

const { WasmReplay, parse_replay_data_wasm, version } = require('./pkg/rosu_replay.js');
const fs = require('fs');

// Read replay file
const replayData = fs.readFileSync('replay.osr');
const replay = new WasmReplay(replayData);

console.log(`Parsed with rosu-replay v${version()}`);
console.log(`Player: ${replay.username}, Score: ${replay.score}`);

๐ŸŽฎ Game Mode Support

This library supports all osu! game modes with mode-specific event data:

Mode Enum Event Type Data Fields
osu!standard GameMode::Std ReplayEventOsu x, y coordinates + key states
osu!taiko GameMode::Taiko ReplayEventTaiko drum position + hit types
osu!catch GameMode::Catch ReplayEventCatch horizontal position + dash state
osu!mania GameMode::Mania ReplayEventMania multi-lane key states

๐Ÿ“ File Format Support

The .osr format is a binary format used by osu! to store replay data. This library handles:

  • โœ… All metadata fields (player, score, mods, timestamp, etc.)
  • โœ… LZMA-compressed replay event data (via liblzma for optimal performance)
  • โœ… Uncompressed replay data
  • โœ… Life bar data parsing and generation
  • โœ… Timestamp conversion (Windows ticks โ†” Unix timestamps)
  • โœ… Both 32-bit and 64-bit replay ID formats
  • โœ… All osu! client versions and replay format variations

๐Ÿ”ง Advanced Usage

Custom Compression Settings

use rosu_replay::{Replay, Packer};

let replay = Replay::from_path("input.osr")?;

// Fastest compression (level 1)
let fast_packer = Packer::new().with_preset(1);
let fast_bytes = replay.pack_with(&fast_packer)?;

// Maximum compression (level 9)
let max_packer = Packer::new().with_preset(9);
let small_bytes = replay.pack_with(&max_packer)?;

// Default compression (level 6) - good balance
let default_bytes = replay.pack()?;

Error Handling

use rosu_replay::{Replay, ReplayError};

match Replay::from_path("maybe_invalid.osr") {
    Ok(replay) => println!("Loaded replay for {}", replay.username),
    Err(ReplayError::Io(e)) => println!("File error: {}", e),
    Err(ReplayError::Parse(e)) => println!("Parse error: {}", e),
    Err(ReplayError::Lzma(e)) => println!("Compression error: {}", e),
    Err(ReplayError::Utf8(e)) => println!("Text encoding error: {}", e),
}

Performance Tips

// For batch processing, reuse the same Packer
let packer = Packer::new().with_preset(6);
for replay_path in replay_files {
    let replay = Replay::from_path(replay_path)?;
    let bytes = replay.pack_with(&packer)?; // Faster than creating new Packer each time
    // Process bytes...
}

// Use uncompressed format for faster repeated access
let replay = Replay::from_path("replay.osr")?;
let uncompressed_bytes = replay.pack_uncompressed()?; // Faster to parse later

๐Ÿ“Š Performance

rosu-replay is designed for high performance:

  • Zero-copy parsing where possible
  • Optimized LZMA compression via liblzma (faster than previous lzma-rs)
  • Efficient memory usage with streaming decompression
  • WASM-optimized builds for web performance

Benchmarks on a typical replay file:

  • Parse: ~1-2ms
  • Pack (compressed): ~3-5ms
  • Pack (uncompressed): ~0.5ms

๐Ÿงช Examples

Check out the examples/ directory for more comprehensive usage:

# Run the basic example
cargo run --example example_1

# Generate documentation with examples
cargo doc --open

# Run tests including WASM features
cargo test --features wasm

๐Ÿ”„ Migration from 0.1.0

If you're upgrading from an earlier version:

  • โœ… API is backward compatible - no breaking changes
  • โœ… Improved performance with liblzma instead of lzma-rs
  • โœ… New WASM support - opt-in with wasm feature
  • โœ… Better error handling with more specific error types

๐Ÿค Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

Development

# Run all tests
cargo test

# Run with WASM features
cargo test --features wasm

# Build documentation
cargo doc --open

# Format code
cargo fmt

# Run linter
cargo clippy

๐Ÿ“œ License

This project is licensed under the MIT License - see the LICENSE file for details.

๐Ÿ™ Acknowledgments

  • kszlim and contributors for the original osrparse Python library
  • The osu! community for documenting the .osr file format
  • ppy for creating osu! and maintaining the replay format
  • The Rust community for excellent crates like liblzma, wasm-bindgen, and chrono

Made with โค๏ธ for the osu! community

Commit count: 7

cargo fmt