16 unstable releases (3 breaking)
| new 0.4.2 | Jan 19, 2026 |
|---|---|
| 0.4.1 | Jan 18, 2026 |
| 0.3.5 | Jan 18, 2026 |
| 0.2.2 | Sep 7, 2025 |
| 0.1.3 | Aug 19, 2025 |
#154 in Game dev
Used in om_fast_parser
415KB
3K
SLoC
MinaCalc Rust Bindings
Safe and ergonomic Rust bindings for the MinaCalc rhythm game difficulty calculator. This crate provides high-level Rust APIs for calculating difficulty scores in rhythm games like StepMania and Etterna.
Features
- Core Difficulty Calculation: Calculate MSD (MinaCalc Skill Difficulty) scores for rhythm game charts
- Multi-rate Support: Get difficulty scores for music rates from 0.7x to 2.0x
- Pattern Analysis: Analyze specific skillsets like stream, jumpstream, handstream, stamina, and more
- Thread-safe: Multi-threaded calculator pool for high-performance applications
- ROX Integration: Parse and analyze rhythm game chart files using the rhythm-open-exchange format
- Utility Functions: Helper functions for pattern analysis and difficulty comparison
- HashMap Conversion: Easy conversion to HashMap format for flexible data handling
Prerequisites
- Rust: Edition 2021 or later
- C++ Compiler: C++17 compatible compiler (MSVC on Windows, GCC/Clang on Unix)
- Build Tools:
ccandbindgen(automatically handled by Cargo)
Installation
Add this to your Cargo.toml:
[dependencies]
minacalc-rs = "0.4.0"
Feature Flags
The crate supports several optional features:
[dependencies]
minacalc-rs = { version = "0.4.0", features = ["hashmap", "thread", "rox", "utils"] }
hashmap(default): Provides HashMap conversion for MSD resultsthread: Provides thread-safe calculator poolrox: Provides chart parsing via rhythm-open-exchange (supports .sm, .osu, .rox formats)utils: Provides utility functions for pattern analysis
Quick Start
Basic Usage
use minacalc_rs::{Calc, Note};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a calculator instance
let calc = Calc::new()?;
// Define some notes (4K chart)
let notes = vec![
Note { notes: 1, row_time: 0.0 }, // Left column at 0s
Note { notes: 8, row_time: 1.0 }, // Right column at 1s
Note { notes: 15, row_time: 2.0 }, // All columns at 2s
];
// Calculate MSD scores for all rates
let msd_results = calc.calc_msd(¬es)?;
// Access scores for specific rates
let overall_1x = msd_results.msds[3].overall; // 1.0x rate
let stream_2x = msd_results.msds[13].stream; // 2.0x rate
println!("1.0x Overall: {:.2}", overall_1x);
println!("2.0x Stream: {:.2}", stream_2x);
Ok(())
}
HashMap Conversion
use minacalc_rs::{Calc, Note, HashMapCalcExt};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let calc = Calc::new()?;
let notes = vec![
Note { notes: 1, row_time: 0.0 },
Note { notes: 8, row_time: 1.0 },
];
let msd_results = calc.calc_msd(¬es)?;
// Convert to HashMap for easy access
let hashmap = msd_results.as_hashmap()?;
// Access by rate string
if let Some(scores) = hashmap.get("1.0") {
println!("1.0x: Overall={:.2}, Stream={:.2}",
scores.overall, scores.stream);
}
Ok(())
}
Chart File Analysis with ROX
use minacalc_rs::{Calc, RoxCalcExt};
use std::path::PathBuf;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let calc = Calc::new()?;
// Analyze a chart file (supports .sm, .osu, .rox formats)
let chart_path = PathBuf::from("path/to/chart.sm");
let msd_results = calc.calc_msd_from_rox_file(&chart_path, 1.0)?;
// Access scores for different rates
let rates = [0.7, 1.0, 1.5, 2.0];
let rate_indices = [0, 3, 8, 13];
for (rate, &index) in rates.iter().zip(rate_indices.iter()) {
if index < msd_results.msds.len() {
let scores = msd_results.msds[index];
println!("{:.1}x: Overall={:.2}, Stream={:.2}, Tech={:.2}",
rate, scores.overall, scores.stream, scores.technical);
}
}
Ok(())
}
Pattern Analysis with Utils
use minacalc_rs::{Calc, Note, utils::*};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let calc = Calc::new()?;
let notes = vec![
Note { notes: 1, row_time: 0.0 },
Note { notes: 8, row_time: 1.0 },
];
let msd_results = calc.calc_msd(¬es)?;
// Get top 3 patterns for 1.0x rate
let top_patterns = calculate_highest_patterns(&msd_results.msds[3], 3);
println!("Top 3 patterns: {:?}", top_patterns);
// Get all patterns ranked by difficulty
let all_patterns = calculate_highest_patterns(&msd_results.msds[3], 7);
println!("All patterns ranked: {:?}", all_patterns);
Ok(())
}
Thread-safe Calculator Pool
use minacalc_rs::thread::{CalcPool, Note};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a pool of calculators
let pool = CalcPool::new(4)?; // 4 calculator instances
let notes = vec![
Note { notes: 1, row_time: 0.0 },
Note { notes: 8, row_time: 1.0 },
];
// Calculate MSD using the pool
let msd_results = pool.calc_msd(¬es)?;
println!("Overall difficulty: {:.2}", msd_results.msds[3].overall);
Ok(())
}
API Reference
Core Types
Calc: Main calculator instanceNote: Represents a note in a rhythm game chartAllRates: Contains MSD scores for all music rates (0.7x to 2.0x)SkillsetScores: Individual skillset scores (stream, jumpstream, etc.)
Skillsets
The calculator provides scores for these skillsets:
- Overall: General difficulty rating
- Stream: Consecutive note patterns
- Jumpstream: Jump patterns in streams
- Handstream: Two-handed patterns
- Stamina: Endurance requirements
- Jackspeed: Jack pattern speed
- Chordjack: Chord jack patterns
- Technical: Technical complexity
Music Rates
Scores are calculated for 14 different music rates:
- 0.7x, 0.8x, 0.9x, 1.0x, 1.1x, 1.2x, 1.3x, 1.4x, 1.5x, 1.6x, 1.7x, 1.8x, 1.9x, 2.0x
Examples
The crate includes several examples demonstrating different use cases:
basic_usage: Basic MSD calculationrox: Chart file analysis with ROXutils_example: Pattern analysis utilities
Run examples with:
# Basic usage
cargo run --example basic_usage
# ROX chart analysis (requires rox feature)
cargo run --example rox --features="rox hashmap"
# Utils example (requires utils feature)
cargo run --example utils_example --features="utils"
Error Handling
The crate uses custom error types for different failure modes:
MinaCalcError: General calculation errorsRoxError: Chart file parsing errors
All functions return Result<T, E> for proper error handling.
Thread Safety
The Calc type is not Send or Sync by default. For multi-threaded applications, use the CalcPool from the thread feature, which provides a thread-safe pool of calculator instances.
Performance
- Single-threaded: Optimized for single-threaded applications
- Multi-threaded: Use
CalcPoolfor concurrent calculations - Memory: Efficient memory usage with minimal allocations
- Caching: Calculator instances can be reused for multiple calculations
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.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Acknowledgments
- MinaCalc: The original C++ difficulty calculator
- Etterna: The rhythm game that inspired this project
- Rust Community: For the excellent tooling and ecosystem
Changelog
v0.4.0
- Unified API: Consolidated calculation methods into generic
calculate_at_rateandcalculate_all_rates. - Capped Parameter: Introduced
cappedparameter to toggle between SSR (capped) and MSD (uncapped/skillset rating) calculations. Defaults tofalse(MSD). - Breaking Change: Removed legacy aliases (
calc_msd,calc_ssr,CalculateSsr,CalculateSsrFromFile) in favor of the new generic API.
v0.3.5
- Fixed score_goal parameter (now uses 0.93 instead of 93.0)
- Synchronized all binding versions
v0.3.1
- Added C# bindings (
bindings/csharp) - Added Python bindings (
bindings/python) - Added
apifeature for FFI exports (file path and string content support) - Added GitHub Actions release workflow (PyPI, Crates.io, NuGet)
v0.3.0
- Removed deprecated
osufeature (useroxinstead for chart parsing) - Updated rhythm-open-exchange dependency
- Updated MinaCalc to 515 version
v0.2.1
- Added
utilsfeature with pattern analysis functions
v0.2.0
- Added
roxfeature for universal chart parsing - Added
threadfeature for thread-safe calculator pools - Improved HashMap conversion utilities
- Enhanced documentation and examples
- Improved type naming (
AllRatesinstead ofMsdForAllRates) - Enhanced error handling and documentation
- Fixed namespace conflicts in bindings
v0.1.0
- Initial release with basic MSD calculation
- Core calculator functionality
- Basic Rust bindings for MinaCalc
Dependencies
~8–12MB
~203K SLoC