use std::{
num::{NonZeroU8, NonZeroU64},
path::PathBuf,
};
use strict_num_extended::{FinF64, NonNegativeF64};
use thiserror::Error;
use crate::{
bms::{command::StringValue, prelude::*},
bmson::{BgaId, Bmson, pulse::PulseNumber},
};
#[derive(Debug, Clone, Copy, Error, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum BmsonToBmsWarning {
#[error("wav object ID was out of range, using default value")]
WavObjIdOutOfRange,
#[error("BGA header object ID was out of range, using default value")]
BgaHeaderObjIdOutOfRange,
#[error("BGA event object ID was out of range, using default value")]
BgaEventObjIdOutOfRange,
#[error("BPM definition was out of range, using default value")]
BpmDefOutOfRange,
#[error("stop definition was out of range, using default value")]
StopDefOutOfRange,
#[error("scroll definition was out of range, using default value")]
ScrollDefOutOfRange,
}
#[derive(Debug, Clone, PartialEq)]
#[must_use]
pub struct BmsonToBmsOutput {
pub bms: Bms,
pub warnings: Vec<BmsonToBmsWarning>,
pub playing_warnings: Vec<PlayingWarning>,
pub playing_errors: Vec<PlayingError>,
}
impl Bms {
pub fn from_bmson(value: Bmson) -> BmsonToBmsOutput {
let mut bms = Self::default();
let mut warnings = Vec::new();
let mut wav_obj_id_issuer = ObjId::all_values();
let mut bga_header_obj_id_issuer = ObjId::all_values();
let mut bpm_def_obj_id_issuer = ObjId::all_values();
let mut stop_def_obj_id_issuer = ObjId::all_values();
let mut scroll_def_obj_id_issuer = ObjId::all_values();
let resolution = value.info.resolution;
bms.music_info.title = Some(value.info.title.into_owned());
bms.music_info.subtitle = Some(value.info.subtitle.into_owned());
bms.music_info.artist = Some(value.info.artist.into_owned());
bms.music_info.sub_artist = value
.info
.subartists
.first()
.map(|s| s.clone().into_owned());
bms.music_info.genre = Some(value.info.genre.into_owned());
bms.metadata.play_level = Some(value.info.level as u8);
let total = value.info.total;
bms.judge.total = Some(StringValue::from_value(total));
bms.sprite.back_bmp = value.info.back_image.map(|s| PathBuf::from(s.into_owned()));
bms.sprite.stage_file = value
.info
.eyecatch_image
.map(|s| PathBuf::from(s.into_owned()));
bms.sprite.banner = value
.info
.banner_image
.map(|s| PathBuf::from(s.into_owned()));
bms.music_info.preview_music = value
.info
.preview_music
.map(|s| PathBuf::from(s.into_owned()));
let judge_rank_value = (value.info.judge_rank.as_f64() * 18.0) as i64;
bms.judge.rank = Some(JudgeLevel::OtherInt(judge_rank_value));
bms.bpm.bpm = Some(StringValue::from_value(value.info.init_bpm));
bms.section_len.section_len_changes.insert(
Track(0),
SectionLenChangeObj {
track: Track(0),
length: FinF64::new(resolution.get() as f64).expect("resolution should be finite"),
},
);
for bpm_event in value.bpm_events {
let time = convert_pulse_to_obj_time(bpm_event.y, resolution);
let bpm = bpm_event.bpm;
let bpm_def_id = bpm_def_obj_id_issuer.next().unwrap_or_else(|| {
warnings.push(BmsonToBmsWarning::BpmDefOutOfRange);
ObjId::null()
});
bms.bpm
.bpm_defs
.insert(bpm_def_id, StringValue::from_value(bpm));
bms.bpm.bpm_changes.insert(time, BpmChangeObj { time, bpm });
}
for stop_event in value.stop_events {
let time = convert_pulse_to_obj_time(stop_event.y, resolution);
let duration = NonNegativeF64::new(stop_event.duration as f64)
.expect("stop duration should be finite");
let stop_def_id = stop_def_obj_id_issuer.next().unwrap_or_else(|| {
warnings.push(BmsonToBmsWarning::StopDefOutOfRange);
ObjId::null()
});
bms.stop
.stop_defs
.insert(stop_def_id, StringValue::from_value(duration));
bms.stop.stops.insert(time, StopObj { time, duration });
}
for scroll_event in value.scroll_events {
let time = convert_pulse_to_obj_time(scroll_event.y, resolution);
let factor = scroll_event.rate;
let scroll_def_id = scroll_def_obj_id_issuer.next().unwrap_or_else(|| {
warnings.push(BmsonToBmsWarning::ScrollDefOutOfRange);
ObjId::null()
});
bms.scroll
.scroll_defs
.insert(scroll_def_id, StringValue::from_value(factor));
bms.scroll
.scrolling_factor_changes
.insert(time, ScrollingFactorObj { time, factor });
}
for sound_channel in value.sound_channels {
let wav_path = PathBuf::from(sound_channel.name.into_owned());
let obj_id = wav_obj_id_issuer.next().unwrap_or_else(|| {
warnings.push(BmsonToBmsWarning::WavObjIdOutOfRange);
ObjId::null()
});
bms.wav.wav_files.insert(obj_id, wav_path);
for note in sound_channel.notes {
let time = convert_pulse_to_obj_time(note.y, resolution);
let (key, side) = convert_lane_to_key_side(note.x);
let kind = if note.l > 0 {
NoteKind::Long
} else {
NoteKind::Visible
};
let obj = WavObj {
offset: time,
channel_id: KeyLayoutBeat::new(side, kind, key).to_channel_id(),
wav_id: obj_id,
};
bms.wav.notes.push_note(obj);
}
}
for mine_channel in value.mine_channels {
let wav_path = PathBuf::from(mine_channel.name.into_owned());
let obj_id = wav_obj_id_issuer.next().unwrap_or_else(|| {
warnings.push(BmsonToBmsWarning::WavObjIdOutOfRange);
ObjId::null()
});
bms.wav.wav_files.insert(obj_id, wav_path);
for mine_event in mine_channel.notes {
let time = convert_pulse_to_obj_time(mine_event.y, resolution);
let (key, side) = convert_lane_to_key_side(mine_event.x);
let obj = WavObj {
offset: time,
channel_id: KeyLayoutBeat::new(side, NoteKind::Landmine, key).to_channel_id(),
wav_id: obj_id,
};
bms.wav.notes.push_note(obj);
}
}
for key_channel in value.key_channels {
let wav_path = PathBuf::from(key_channel.name.into_owned());
let obj_id = wav_obj_id_issuer.next().unwrap_or_else(|| {
warnings.push(BmsonToBmsWarning::WavObjIdOutOfRange);
ObjId::null()
});
bms.wav.wav_files.insert(obj_id, wav_path);
for key_event in key_channel.notes {
let time = convert_pulse_to_obj_time(key_event.y, resolution);
let (key, side) = convert_lane_to_key_side(key_event.x);
let obj = WavObj {
offset: time,
channel_id: KeyLayoutBeat::new(side, NoteKind::Invisible, key).to_channel_id(),
wav_id: obj_id,
};
bms.wav.notes.push_note(obj);
}
}
let mut bga_id_to_obj_id = std::collections::HashMap::new();
for bga_header in value.bga.bga_header {
let bmp_path = PathBuf::from(bga_header.name.into_owned());
let obj_id = bga_header_obj_id_issuer.next().unwrap_or_else(|| {
warnings.push(BmsonToBmsWarning::BgaHeaderObjIdOutOfRange);
ObjId::null()
});
bga_id_to_obj_id.insert(bga_header.id, obj_id);
bms.bmp.bmp_files.insert(
obj_id,
Bmp {
file: bmp_path,
transparent_color: Argb::default(),
},
);
}
let mut get_bga_obj_id = |bga_id: &BgaId| -> ObjId {
bga_id_to_obj_id.get(bga_id).copied().unwrap_or_else(|| {
warnings.push(BmsonToBmsWarning::BgaEventObjIdOutOfRange);
ObjId::null()
})
};
for bga_event in value.bga.bga_events {
let time = convert_pulse_to_obj_time(bga_event.y, resolution);
let obj_id = get_bga_obj_id(&bga_event.id);
bms.bmp.bga_changes.insert(
time,
BgaObj {
time,
id: obj_id,
layer: BgaLayer::Base,
},
);
}
for bga_event in value.bga.layer_events {
let time = convert_pulse_to_obj_time(bga_event.y, resolution);
let obj_id = get_bga_obj_id(&bga_event.id);
bms.bmp.bga_changes.insert(
time,
BgaObj {
time,
id: obj_id,
layer: BgaLayer::Overlay,
},
);
}
for bga_event in value.bga.poor_events {
let time = convert_pulse_to_obj_time(bga_event.y, resolution);
let obj_id = get_bga_obj_id(&bga_event.id);
bms.bmp.bga_changes.insert(
time,
BgaObj {
time,
id: obj_id,
layer: BgaLayer::Poor,
},
);
}
let PlayingCheckOutput {
playing_warnings,
playing_errors,
} = bms.check_playing::<KeyLayoutBeat>();
BmsonToBmsOutput {
bms,
warnings,
playing_warnings,
playing_errors,
}
}
}
fn convert_pulse_to_obj_time(pulse: PulseNumber, resolution: NonZeroU64) -> ObjTime {
let pulses_per_measure = resolution.get().checked_mul(4).unwrap_or_else(|| {
panic!(
"resolution too large: {}. Maximum supported value is u64::MAX / 4 = {}",
resolution.get(),
u64::MAX / 4
);
}); let track = pulse.0 / pulses_per_measure;
let remaining_pulses = pulse.0 % pulses_per_measure;
let numerator = remaining_pulses;
let denominator = pulses_per_measure;
ObjTime::new(track, numerator, denominator)
.expect("pulses_per_measure should be non-zero after overflow check")
}
fn convert_lane_to_key_side(lane: Option<NonZeroU8>) -> (Key, PlayerSide) {
let lane_value = lane.map_or(0, std::num::NonZero::get);
let (adjusted_lane, side) = if lane_value > 8 {
(lane_value - 8, PlayerSide::Player2)
} else {
(lane_value, PlayerSide::Player1)
};
let key = match adjusted_lane {
key @ 1..=7 => Key::Key(key),
8 => Key::Scratch(1),
_ => Key::Key(1), };
(key, side)
}