diff --git a/Cargo.lock b/Cargo.lock index f26cc09..2512e4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -835,6 +835,15 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + [[package]] name = "enumset" version = "1.0.12" @@ -1772,6 +1781,7 @@ dependencies = [ "home", "ordered-float", "rodio", + "symphonia", ] [[package]] @@ -2172,6 +2182,152 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "symphonia" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3671dd6f64f4f9d5c87179525054cfc1f60de23ba1f193bd6ceab812737403f1" +dependencies = [ + "lazy_static", + "symphonia-bundle-flac", + "symphonia-bundle-mp3", + "symphonia-codec-adpcm", + "symphonia-codec-pcm", + "symphonia-codec-vorbis", + "symphonia-core", + "symphonia-format-mkv", + "symphonia-format-ogg", + "symphonia-format-wav", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-flac" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dc2deed3204967871ba60f913378f95820cb47a2fe9b2eef5a9eedb417dfdc8" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a0846e7a2c9a8081ff799fc83a975170417ad2a143f644a77ec2e3e82a2b73" +dependencies = [ + "bitflags", + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-codec-adpcm" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a5cfb8d4405e26eb9593157dc45b05e102b8d774b38ed2a95946d6bb9e26e3e" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb9a9f0b9991cccf3217b74644af412d5d082a4815e5e2943f26e0ecabdf3c9" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfed6f7b6bfa21d7cef1acefc8eae5db80df1608a1aca91871b07cbd28d7b74" +dependencies = [ + "log", + "symphonia-core", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-core" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9567e2d8a5f866b2f94f5d366d811e0c6826babcff6d37de9e1a6690d38869" +dependencies = [ + "arrayvec 0.7.2", + "bitflags", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-format-mkv" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd22f2def8c8f078495ad66111648bfc7d5222ee33774f2077cb665588f3119" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-ogg" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "474df6e86b871dcb56913130bada1440245f483057c4a2d8a2981455494c4439" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-wav" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06679bd5646b3037300f88891dfc8a6e1cc4e1133206cc17a98e5d7c22f88296" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acd35c263223ef6161000be79b124a75de3e065eea563bf3ef169b3e94c7bb2e" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-utils-xiph" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce340a6c33ac06cb42de01220308ec056e8a2a3d5cc664aaf34567392557136b" +dependencies = [ + "symphonia-core", + "symphonia-metadata", +] + [[package]] name = "syn" version = "1.0.107" diff --git a/Cargo.toml b/Cargo.toml index 61dca13..158daa4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,4 @@ flate2 = {version = "1.0.25"} gd_plist = {git = "https://github.com/Syudagye/gd-plist.git", version = "1.4.0"} chrono = {version = "0.4.23"} ordered-float = {version = "3.4.0"} +symphonia = {version = "0.5.2", features = ["mp3"]} diff --git a/src/gd.rs b/src/gd.rs index 0464c94..54ec6a2 100644 --- a/src/gd.rs +++ b/src/gd.rs @@ -1,11 +1,11 @@ -use std::path::PathBuf; -use std::fs::File; -use std::io::{Read, Cursor}; use base64::engine::{general_purpose::URL_SAFE, Engine}; +use eframe::egui::TextFormat; +use eframe::epaint::text::LayoutJob; use flate2::read::GzDecoder; use gd_plist::Value; -use eframe::epaint::text::LayoutJob; -use eframe::egui::TextFormat; +use std::fs::File; +use std::io::{Cursor, Read}; +use std::path::PathBuf; struct User { name: String, @@ -13,9 +13,9 @@ struct User { } enum Song { - Official{id: i32 /*k8*/}, - Newgrounds{id: i32 /*k45*/}, - Unknown + Official { id: i32 /*k8*/ }, + Newgrounds { id: i32 /*k45*/ }, + Unknown, } struct Level { @@ -25,15 +25,22 @@ struct Level { #[derive(Debug)] pub struct OuterLevel { - name: String, // k2 + name: String, // k2 revision: Option, // k46 } impl OuterLevel { pub fn load_all() -> Vec { let plist = get_local_level_plist(); - let levels: Vec = plist.as_dictionary().and_then(|dict| dict.get("LLM_01")).unwrap() - .as_dictionary().unwrap().into_iter().filter(|(key, _)| key.as_str() != "_isArr").map(|(_, val)| { + let levels: Vec = plist + .as_dictionary() + .and_then(|dict| dict.get("LLM_01")) + .unwrap() + .as_dictionary() + .unwrap() + .into_iter() + .filter(|(key, _)| key.as_str() != "_isArr") + .map(|(_, val)| { let mut builder = LevelBuilder::new(); let props = val.as_dictionary().unwrap(); if let Some(title) = props.get("k2") { @@ -43,7 +50,8 @@ impl OuterLevel { builder.with_revision(rev.as_signed_integer().unwrap().into()); } builder.build_outer_level().unwrap() - }).collect(); + }) + .collect(); levels } @@ -52,17 +60,21 @@ impl OuterLevel { Some(rev) => { let mut job = LayoutJob::default(); job.append(&format!("{} ", self.name), 0f32, TextFormat::default()); - job.append(&format!("(rev {})", rev), 0f32, TextFormat { - italics: true, - ..Default::default() - }); + job.append( + &format!("(rev {})", rev), + 0f32, + TextFormat { + italics: true, + ..Default::default() + }, + ); job - }, + } None => { let mut job = LayoutJob::default(); job.append(&self.name, 0f32, TextFormat::default()); job - }, + } } } } @@ -70,10 +82,24 @@ impl OuterLevel { pub fn gd_path() -> PathBuf { let mut path_buf = home::home_dir().unwrap(); #[cfg(unix)] - path_buf.extend([".local", "share", "Steam", "steamapps", - "compatdata", "322170", "pfx", "drive_c", - "users", "steamuser", "AppData", "Local", - "GeometryDash"].iter()); + path_buf.extend( + [ + ".local", + "share", + "Steam", + "steamapps", + "compatdata", + "322170", + "pfx", + "drive_c", + "users", + "steamuser", + "AppData", + "Local", + "GeometryDash", + ] + .iter(), + ); #[cfg(windows)] path_buf.extend(["AppData", "Local", "GeometryDash"].iter()); path_buf @@ -87,12 +113,18 @@ struct LevelBuilder { impl Default for LevelBuilder { fn default() -> Self { - Self {name: None, song: None, revision: None} + Self { + name: None, + song: None, + revision: None, + } } } impl LevelBuilder { - fn new() -> Self {Self::default()} + fn new() -> Self { + Self::default() + } fn with_name(&mut self, name: String) { self.name = Some(name); @@ -109,10 +141,13 @@ impl LevelBuilder { fn build_level(self) -> Option { match self { Self { - name: Some(name), - song: Some(song), + name: Some(name), + song: Some(song), revision, - } => Some(Level{song, outer: OuterLevel {name, revision}}), + } => Some(Level { + song, + outer: OuterLevel { name, revision }, + }), _ => None, } } @@ -120,10 +155,10 @@ impl LevelBuilder { fn build_outer_level(self) -> Option { match self { Self { - name: Some(name), + name: Some(name), revision, .. - } => Some(OuterLevel {name, revision}), + } => Some(OuterLevel { name, revision }), _ => None, } } @@ -131,12 +166,17 @@ impl LevelBuilder { fn get_local_level_plist() -> Value { let raw_save_data = { - let mut save_file = File::open(gd_path().join("CCLocalLevels.dat")).expect("No save file found!"); + let mut save_file = + File::open(gd_path().join("CCLocalLevels.dat")).expect("No save file found!"); let mut sd = Vec::new(); save_file.read_to_end(&mut sd).unwrap(); sd }; - let data_post_xor: Vec = raw_save_data.iter().map(|b| b ^ 11).filter(|&b| b != 0u8).collect(); + let data_post_xor: Vec = raw_save_data + .iter() + .map(|b| b ^ 11) + .filter(|&b| b != 0u8) + .collect(); let data_post_b64 = URL_SAFE.decode(data_post_xor).unwrap(); let mut decoder = GzDecoder::<&[u8]>::new(data_post_b64.as_ref()); let mut plist = String::new(); @@ -145,4 +185,3 @@ fn get_local_level_plist() -> Value { } Value::from_reader(Cursor::new(plist)).unwrap() } - diff --git a/src/main.rs b/src/main.rs index a3342e5..6a0d1c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,12 +5,11 @@ use eframe; use eframe::egui; use std::boxed::Box; use std::collections::VecDeque; - +use std::marker::PhantomData; struct PipeDash { msg_queue: VecDeque, selected_level: Option, - selected_color: Option, level_list: Vec, editor: Editor, } @@ -24,7 +23,6 @@ enum Color { #[derive(Debug)] enum Message { - ColorSelected(Color), LevelSelected(usize), } @@ -34,38 +32,78 @@ struct Editor { beats_per_bar: f32, subdivisions: f32, beat_rate: music::BeatRate, + time_signatures: music::TimeSignature, + green_lines: music::Lines, + orange_lines: music::Lines, + yellow_lines: music::Lines, } -type EditorWidget<'a> = &'a mut Editor; +struct Orange; +struct Yellow; +struct Green; + +struct BeatRateWidget<'a>(&'a mut Editor); +struct TimeSignatureWidget<'a>(&'a mut Editor); +struct LinesWidget<'a, C: WithColor>(&'a mut Editor, PhantomData); + +trait WithColor {} + +impl WithColor for Orange {} +impl WithColor for Yellow {} +impl WithColor for Green {} impl Editor { - pub fn widget(&mut self) -> EditorWidget { - self + pub fn beat_rate_widget(&mut self) -> BeatRateWidget { + BeatRateWidget(self) + } + + pub fn time_signature_widget(&mut self) -> TimeSignatureWidget { + TimeSignatureWidget(self) + } + + pub fn lines_widget(&mut self) -> LinesWidget { + LinesWidget(self, Default::default()) } } -impl<'a> egui::Widget for EditorWidget<'a> { +impl<'a> egui::Widget for BeatRateWidget<'a> { fn ui(self, ui: &mut egui::Ui) -> egui::Response { // 1. choose size let max_rect = ui.max_rect(); - let preferred_size = max_rect.size(); + let preferred_size = egui::Vec2::new(max_rect.size().x, 60.0); // 2. allocate space let (rect, res) = ui.allocate_exact_size(preferred_size, egui::Sense::click_and_drag()); // 3. handle interactions // 4. draw widget if ui.is_rect_visible(rect) { - ui.painter().rect_filled(rect, 0f32, eframe::epaint::Color32::from_gray(0)); + ui.painter() + .rect_filled(rect, 0f32, eframe::epaint::Color32::from_gray(0)); } res } } +impl<'a> egui::Widget for TimeSignatureWidget<'a> { + fn ui(self, ui: &mut egui::Ui) -> egui::Response { + // 1. choose size + let max_rect = ui.max_rect(); + let preferred_size = egui::Vec2::new(max_rect.size().x, 60.0); + // 2. allocate space + let (rect, res) = ui.allocate_exact_size(preferred_size, egui::Sense::click_and_drag()); + // 3. handle interactions + // 4. draw widget + if ui.is_rect_visible(rect) { + ui.painter() + .rect_filled(rect, 0f32, eframe::epaint::Color32::from_gray(0)); + } + res + } +} impl PipeDash { fn new(_cc: &eframe::CreationContext) -> Self { Self { selected_level: None, - selected_color: None, msg_queue: VecDeque::new(), level_list: gd::OuterLevel::load_all(), editor: Editor { @@ -74,49 +112,44 @@ impl PipeDash { beats_per_bar: 4.0, subdivisions: 4.0, beat_rate: music::StaticBeatRate::from_bpm(120f32).into(), - } + time_signatures: music::StaticTimeSignature::new(4, 4).into(), + green_lines: music::Lines::new(), + orange_lines: music::Lines::new(), + yellow_lines: music::Lines::new(), + }, } } fn side_panel(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { - egui::SidePanel::left("level_picker").default_width(100f32).show(ctx, |ui| { - egui::ScrollArea::vertical().show(ui, |ui| { - ui.with_layout(egui::Layout::top_down_justified(egui::Align::Min), |ui| { - for (idx, level) in self.level_list.iter().enumerate() { - if ui.selectable_label(self.selected_level == Some(idx), level.display_name()).clicked() { - self.msg_queue.push_back(Message::LevelSelected(idx)); + egui::SidePanel::left("level_picker") + .default_width(100f32) + .show(ctx, |ui| { + egui::ScrollArea::vertical().show(ui, |ui| { + ui.with_layout(egui::Layout::top_down_justified(egui::Align::Min), |ui| { + for (idx, level) in self.level_list.iter().enumerate() { + if ui + .selectable_label( + self.selected_level == Some(idx), + level.display_name(), + ) + .clicked() + { + self.msg_queue.push_back(Message::LevelSelected(idx)); + } } - } - }) + }) + }); }); - }); } fn center_panel(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { - use Message::*; - use Color::*; egui::CentralPanel::default().show(ctx, |ui| { ui.vertical_centered_justified(|ui| { - ui.horizontal_top(|ui| { - ui.vertical(|ui| { - ui.label("Song name"); - ui.label("Song id"); - }); - ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { - ui.vertical(|ui| { - if ui.selectable_label(self.selected_color == Some(Orange), "orange").clicked() { - self.msg_queue.push_back(ColorSelected(Orange)); - } - if ui.selectable_label(self.selected_color == Some(Yellow), "yellow").clicked() { - self.msg_queue.push_back(ColorSelected(Yellow)); - } - if ui.selectable_label(self.selected_color == Some(Green), "green").clicked() { - self.msg_queue.push_back(ColorSelected(Green)); - } - }); - }) + ui.vertical(|ui| { + ui.label("Song name"); + ui.label("Song id"); }); - ui.add(self.editor.widget()); + ui.add(self.editor.beat_rate_widget()); }); }); } @@ -125,12 +158,9 @@ impl PipeDash { for message in self.msg_queue.drain(..) { println!("{:?}", message); match message { - Message::ColorSelected(color) => { - self.selected_color = Some(color); - }, Message::LevelSelected(idx) => { self.selected_level = Some(idx); - }, + } } } } @@ -152,4 +182,3 @@ fn main() { let opts = eframe::NativeOptions::default(); eframe::run_native("PipeDash", opts, Box::new(|cc| Box::new(PipeDash::new(cc)))); } - diff --git a/src/music.rs b/src/music.rs index 7854865..3c006be 100644 --- a/src/music.rs +++ b/src/music.rs @@ -1,6 +1,6 @@ use chrono::Duration; -use std::collections::BTreeMap; use ordered_float::OrderedFloat as Float; +use std::collections::{BTreeMap, BTreeSet}; pub type BeatPosition = Float; @@ -25,6 +25,10 @@ pub struct StaticTimeSignature { denominator: u32, } +#[derive(Default)] +pub struct Lines { + positions: BTreeSet, +} impl StaticBeatRate { pub fn from_bpm(bpm: f32) -> Self { @@ -48,13 +52,19 @@ impl BeatRate { if &pos < first_change { self.initial } else { - *self.changes.iter().rev().find(|&el| el.0 <= &pos).unwrap().1 + *self + .changes + .iter() + .rev() + .find(|&el| el.0 <= &pos) + .unwrap() + .1 } - }, + } None => self.initial, } } - + pub fn add_change(&mut self, new_pos: BeatPosition, new_rate: StaticBeatRate) { self.changes.insert(new_pos, new_rate); } @@ -62,9 +72,16 @@ impl BeatRate { /// Changes: when the time signature changes, the bar immediately resets impl StaticTimeSignature { - pub fn new(numerator: u32, denominator: u32) -> Self {Self {numerator, denominator}} + pub fn new(numerator: u32, denominator: u32) -> Self { + Self { + numerator, + denominator, + } + } - fn beats_per_bar(&self) -> BeatPosition {(self.numerator as f32).into()} + fn beats_per_bar(&self) -> BeatPosition { + (self.numerator as f32).into() + } } impl From for TimeSignature { @@ -87,28 +104,52 @@ impl TimeSignature { if &pos < first_change { self.initial } else { - *self.changes.iter().rev().find(|&el| el.0 <= &pos).unwrap().1 + *self + .changes + .iter() + .rev() + .find(|&el| el.0 <= &pos) + .unwrap() + .1 } - }, + } none => self.initial, } } pub fn position_in_bar(&self, pos: BeatPosition) -> BeatPosition { match self.changes.first_key_value() { - Some((first_change, first_change_sig)) => { + Some((first_change, _)) => { if &pos < first_change { pos % self.initial.beats_per_bar() } else { - let (signature_start_point, signature) = self.changes.iter().rev().find(|&el| el.0 <= &pos).unwrap(); + let (signature_start_point, signature) = + self.changes.iter().rev().find(|&el| el.0 <= &pos).unwrap(); (pos - signature_start_point) % signature.beats_per_bar() } - }, + } None => pos % self.initial.beats_per_bar(), } } } +impl Lines { + pub fn new() -> Self { + Self::default() + } + + pub fn insert(&mut self, pos: BeatPosition) -> bool { + self.positions.insert(pos) + } + + pub fn remove(&mut self, pos: BeatPosition) -> bool { + self.positions.remove(&pos) + } + + pub fn get_positions(&mut self) -> &BTreeSet { + &self.positions + } +} #[cfg(test)] mod tests {