This repository has been archived on 2024-01-28. You can view files and clone it, but cannot push or open issues/pull-requests.
brd/src/converter/ddr2osu.rs

422 lines
13 KiB
Rust

use std::fmt;
use std::str::FromStr;
use anyhow::{anyhow, Result};
use clap::Clap;
use log::{debug, info, trace};
use crate::ddr::ssq;
use crate::osu::beatmap;
#[derive(Debug)]
pub struct ConfigRange(f32, f32);
impl ConfigRange {
/// Map value from 0 to 1 onto the range
fn map_from(&self, value: f32) -> f32 {
(value * (self.1 - self.0)) + self.0
}
}
impl fmt::Display for ConfigRange {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}:{}", self.0, self.1)
}
}
impl FromStr for ConfigRange {
type Err = anyhow::Error;
fn from_str(string: &str) -> Result<Self> {
match string.split(':').collect::<Vec<&str>>()[..] {
[start, end] => Ok(ConfigRange(start.parse::<f32>()?, end.parse::<f32>()?)),
_ => Err(anyhow!("Invalid range format (expected start:end)")),
}
}
}
#[derive(Debug, Clap)]
pub struct Config {
#[clap(skip = "audio.wav")]
pub audio_filename: String,
#[clap(
long,
default_value = "180",
about = "Offset in milliseconds",
display_order = 5
)]
pub offset: i32,
#[clap(
long = "no-stops",
about = "Disable stops",
parse(from_flag = std::ops::Not::not),
display_order = 5
)]
pub stops: bool,
#[clap(
arg_enum,
long,
default_value = "step",
about = "What to do with shocks",
display_order = 5
)]
pub shock_action: ShockAction,
#[clap(
long = "hp",
about = "Range of HP drain (beginner:challenge)",
default_value = "4:8"
)]
pub hp_drain: ConfigRange,
#[clap(
long = "acc",
about = "Range of Accuracy (beginner:challenge)",
default_value = "7:8"
)]
pub accuracy: ConfigRange,
#[clap(flatten)]
pub metadata: ConfigMetadata,
}
#[derive(Clap, Debug)]
pub struct ConfigMetadata {
#[clap(long, about = "Song title to use in beatmap", display_order = 6)]
pub title: String,
#[clap(long, about = "Artist name to use in beatmap", display_order = 6)]
pub artist: String,
#[clap(
long,
default_value = "Dance Dance Revolution",
about = "Source to use in beatmap",
display_order = 6
)]
pub source: String,
}
impl fmt::Display for Config {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"ddr2osu (+{}ms{} shock→{:?} hp{} acc{})",
self.offset,
if self.stops { " stops" } else { "" },
self.shock_action,
self.hp_drain,
self.accuracy
)
}
}
#[derive(Clap, Clone, Debug)]
pub enum ShockAction {
Ignore,
Step,
//Static(Vec<u8>),
}
struct ShockStepGenerator {
last: u8,
columns: u8,
mode: ShockAction,
}
impl Iterator for ShockStepGenerator {
type Item = Vec<u8>;
fn next(&mut self) -> Option<Vec<u8>> {
match &self.mode {
ShockAction::Ignore => None,
ShockAction::Step => {
let columns = match self.last {
0 | 3 => vec![0, 3],
1 | 2 => vec![1, 2],
4 | 7 => vec![4, 7],
5 | 6 => vec![5, 6],
_ => vec![],
};
self.last = (self.last + 1) % self.columns;
Some(columns)
} //ShockAction::Static(columns) => Some(columns.clone()),
}
}
}
impl ShockStepGenerator {
fn new(columns: u8, mode: ShockAction) -> Self {
Self {
last: 0,
columns,
mode,
}
}
}
fn get_time_from_beats(beats: f32, tempo_changes: &[ssq::TempoChange]) -> Result<i32> {
for tempo_change in tempo_changes {
// For TempoChanges that are infinitely short but exactly cover that beat, use the start
// time of that TempoChange
if (beats - tempo_change.start_beats).abs() < 0.001
&& (beats - tempo_change.end_beats).abs() < 0.001
{
return Ok(tempo_change.start_ms);
}
if beats < tempo_change.end_beats {
return Ok(tempo_change.start_ms
+ ((beats - tempo_change.start_beats) * tempo_change.beat_length) as i32);
}
}
Err(anyhow!(
"Conversion of Step to HitObject failed: Beat lies outside of TimingPoints range"
))
}
impl From<ssq::TempoChange> for beatmap::TimingPoint {
fn from(tempo_change: ssq::TempoChange) -> Self {
beatmap::TimingPoint {
time: tempo_change.start_ms,
beat_length: if tempo_change.beat_length == f32::INFINITY {
10000.0
} else {
tempo_change.beat_length
},
meter: 4,
sample_set: beatmap::SampleSet::BeatmapDefault,
sample_index: 0,
volume: 100,
uninherited: true,
effects: beatmap::TimingPointEffects {
kiai_time: false,
omit_first_barline: false,
},
}
}
}
impl ssq::Step {
fn to_hit_objects(
&self,
num_columns: u8,
tempo_changes: &ssq::TempoChanges,
shock_step_generator: &mut ShockStepGenerator,
) -> Result<Vec<beatmap::HitObject>> {
let mut hit_objects = Vec::new();
match self {
ssq::Step::Step { beats, row } => {
let time = get_time_from_beats(*beats, &tempo_changes.0)?;
let columns: Vec<bool> = row.clone().into();
for (column, active) in columns.iter().enumerate() {
if *active {
hit_objects.push(beatmap::HitObject::HitCircle {
x: beatmap::column_to_x(column as u8, num_columns),
y: 192,
time,
hit_sound: beatmap::HitSound {
normal: true,
whistle: false,
finish: false,
clap: false,
},
new_combo: false,
skip_combo_colours: 0,
hit_sample: beatmap::HitSample {
normal_set: 0,
addition_set: 0,
index: 0,
volume: 0,
filename: "".to_string(),
},
})
}
}
}
ssq::Step::Freeze { start, end, row } => {
let time = get_time_from_beats(*start, &tempo_changes.0)?;
let end_time = get_time_from_beats(*end, &tempo_changes.0)?;
let columns: Vec<bool> = row.clone().into();
for (column, active) in columns.iter().enumerate() {
if *active {
hit_objects.push(beatmap::HitObject::Hold {
column: column as u8,
columns: num_columns,
time,
end_time,
hit_sound: beatmap::HitSound {
normal: true,
whistle: false,
finish: true,
clap: false,
},
new_combo: false,
skip_combo_colours: 0,
hit_sample: beatmap::HitSample {
normal_set: 0,
addition_set: 0,
index: 0,
volume: 0,
filename: "".to_string(),
},
})
}
}
}
ssq::Step::Shock { beats } => {
let columns = match shock_step_generator.next() {
Some(columns) => columns,
None => vec![],
};
for column in columns {
hit_objects.push(beatmap::HitObject::HitCircle {
x: beatmap::column_to_x(column as u8, num_columns),
y: 192,
time: get_time_from_beats(*beats, &tempo_changes.0)?,
hit_sound: beatmap::HitSound {
normal: true,
whistle: false,
finish: false,
clap: false,
},
new_combo: false,
skip_combo_colours: 0,
hit_sample: beatmap::HitSample {
normal_set: 0,
addition_set: 0,
index: 0,
volume: 0,
filename: "".to_string(),
},
})
}
}
}
Ok(hit_objects)
}
}
struct ConvertedChart {
difficulty: ssq::Difficulty,
hit_objects: beatmap::HitObjects,
timing_points: beatmap::TimingPoints,
}
impl ConvertedChart {
fn to_beatmap(&self, config: &Config) -> beatmap::Beatmap {
beatmap::Beatmap {
version: 14,
general: beatmap::General {
audio_filename: config.audio_filename.clone(),
audio_lead_in: 0,
preview_time: 0,
countdown: beatmap::Countdown::No,
sample_set: beatmap::SampleSet::Soft,
mode: beatmap::Mode::Mania,
},
editor: beatmap::Editor {},
metadata: beatmap::Metadata {
title: config.metadata.title.clone(),
artist: config.metadata.artist.clone(),
creator: format!("{}", config),
version: format!("{}", self.difficulty),
source: config.metadata.source.clone(),
tags: vec![],
},
difficulty: beatmap::Difficulty {
hp_drain_rate: config.hp_drain.map_from(self.difficulty.clone().into()),
circle_size: self.difficulty.players as f32 * 4.0,
overall_difficulty: config.accuracy.map_from(self.difficulty.clone().into()),
approach_rate: 8.0,
slider_multiplier: 0.64,
slider_tick_rate: 1.0,
},
events: beatmap::Events(vec![]),
timing_points: self.timing_points.clone(),
colours: beatmap::Colours(vec![]),
hit_objects: self.hit_objects.clone(),
}
}
}
impl ssq::SSQ {
pub fn to_beatmaps(&self, config: &Config) -> Result<Vec<beatmap::Beatmap>> {
debug!("Configuration: {:?}", config);
let mut timing_points = Vec::new();
timing_points.push(beatmap::TimingPoint {
time: 0,
beat_length: config.offset as f32,
meter: 4,
sample_set: beatmap::SampleSet::BeatmapDefault,
sample_index: 0,
volume: 0,
uninherited: true,
effects: beatmap::TimingPointEffects {
kiai_time: false,
omit_first_barline: false,
},
});
for entry in &self.tempo_changes.0 {
if config.stops || entry.beat_length != f32::INFINITY {
trace!("Converting {:?} to to timing point", entry);
let timing_point: beatmap::TimingPoint = entry.clone().into();
timing_points.push(timing_point);
}
}
debug!(
"Converted {} tempo changes to timing points",
self.tempo_changes.0.len()
);
let mut converted_charts = Vec::new();
for chart in &self.charts {
debug!("Converting chart {} to beatmap", chart.difficulty);
let mut hit_objects = beatmap::HitObjects(Vec::new());
let mut shock_step_generator =
ShockStepGenerator::new(chart.difficulty.players * 4, config.shock_action.clone());
for step in &chart.steps.0 {
trace!("Converting {:?} to hit object", step);
let mut step_hit_objects = step.to_hit_objects(
chart.difficulty.players * 4,
&self.tempo_changes,
&mut shock_step_generator,
)?;
hit_objects.0.append(&mut step_hit_objects);
}
let converted_chart = ConvertedChart {
difficulty: chart.difficulty.clone(),
hit_objects,
timing_points: beatmap::TimingPoints(timing_points.clone()),
};
debug!(
"Converted to beatmap with {} hit objects",
converted_chart.hit_objects.0.len(),
);
converted_charts.push(converted_chart);
}
let mut beatmaps = Vec::new();
for converted_chart in converted_charts {
let beatmap = converted_chart.to_beatmap(config);
beatmaps.push(beatmap);
}
info!("Converted {} step charts to beatmaps", beatmaps.len());
Ok(beatmaps)
}
}