From 821318a5ad5616dea93ee28b271dc68129ec2dce Mon Sep 17 00:00:00 2001 From: aprzn Date: Tue, 28 Feb 2023 01:02:17 -0500 Subject: [PATCH] cleanup, working on song loading, getting started with wizard (haven't made big transition yet) --- ' | 394 ---------------------------------------------------- src/gd.rs | 45 +++--- src/main.rs | 58 ++++++-- 3 files changed, 67 insertions(+), 430 deletions(-) delete mode 100644 ' diff --git a/' b/' deleted file mode 100644 index 66d45b9..0000000 --- a/' +++ /dev/null @@ -1,394 +0,0 @@ -#![feature(iter_array_chunks)] -#![warn(clippy::all, clippy::pedantic, clippy::nursery)] -#![allow(dead_code)] - -mod gd; -mod music; - -use eframe::egui; -use reqwest::blocking as req; -use std::boxed::Box; -use std::collections::VecDeque; -use std::error::Error; -use std::fs::File; -use std::io::Write; -use std::mem; -use thiserror::Error; - - -struct PipeDash { - project_dirs: directories::ProjectDirs, - msg_queue: VecDeque, - selected_level: Option, - level_list: Vec, - loaded_song: Option, - loaded_level_checksum: Option<(gd::OuterLevel, md5::Digest)>, - editor: Editor, - error: Option>, -} - -#[derive(Debug, PartialEq, Eq)] -enum Color { - Orange, - Yellow, - Green, -} - -#[derive(Debug)] -enum Message { - LevelSelected(usize), - CloseError, - LoadLevel, -} - -struct Editor { - state: EditorState, - data: GdlData, -} - -struct EditorState { - scroll_pos: f32, - pts_per_second: f32, // zoom level - subdivisions: f32, -} - -struct GdlData { - green_lines: music::Lines, - orange_lines: music::Lines, - yellow_lines: music::Lines, - beat_rate: music::BeatRate, - time_signatures: music::TimeSignature, -} - -struct Song { - name: String, - id: i32, - decoder: rodio::Decoder, -} - -#[derive(Error, Debug)] -enum SongError { - #[error("Not a Newgrounds song")] - NotNewgrounds, - #[error("Song mp3 couldn't be downloaded")] - MissingFile(#[from] std::io::Error), - #[error("Couldn't decode mp3 file")] - BrokenSong(#[from] rodio::decoder::DecoderError), - #[error("Couldn't access song data on servers")] - GdServerError(#[from] gd::SongRequestError), - #[error("Couldn't fetch song from Newgrounds")] - NgServerError(#[from] reqwest::Error), - #[error("Missing download link")] - MissingLink, -} - -struct BeatRateWidget<'a> { - state: &'a mut EditorState, - beat_rate: &'a mut music::BeatRate, -} -struct TimeSignatureWidget<'a> { - state: &'a mut EditorState, - time_signatures: &'a mut music::TimeSignature, -} -struct LinesWidget<'a> { - state: &'a mut EditorState, - lines: &'a mut music::Lines, - color: Color, -} -struct WaveformWidget<'a> { - state: &'a mut EditorState, - song: &'a Song, -} - -fn allocate_editor_space(ui: &mut egui::Ui) -> (egui::Rect, egui::Response) { - let max_rect = ui.max_rect(); - let preferred_size = egui::Vec2::new(max_rect.size().x, 60.0); - ui.allocate_exact_size(preferred_size, egui::Sense::click_and_drag()) -} - -impl From for eframe::epaint::Color32 { - fn from(rhs: Color) -> Self { - match rhs { - Color::Green => Self::from_rgb(0, 255, 0), - Color::Orange => Self::from_rgb(255, 127, 0), - Color::Yellow => Self::from_rgb(255, 255, 0), - } - } -} - -impl Song { - pub fn try_new(gd_song: &gd::Song) -> Result { - if let &gd::Song::Newgrounds { id } = gd_song { - let song_response = gd_song.get_response(); - let song_path = gd::save_path().join(format!("{id}.mp3")); - - let (file, name) = match (File::open(&song_path), song_response) { - (Ok(file), response) => { - let name = response - .ok() - .and_then(|response| response.name().map(Into::into)) - .unwrap_or_default(); - - (file, name) - } - (Err(_), Ok(response)) => { - let song_blob = response - .download_link() - .ok_or(SongError::MissingLink) - .and_then(|link| Ok(req::get(link)?.bytes()?))?; - - let mut file = File::open(&song_path)?; - file.write_all(&song_blob)?; - - (file, response.name().unwrap_or("").into()) - } - (Err(err), Err(_)) => return Err(err.into()), - }; - - let decoder = rodio::Decoder::new_mp3(file)?; - - Ok(Self { name, id, decoder }) - } else { - Err(SongError::NotNewgrounds) - } - } -} - -impl Editor { - pub fn beat_rate_widget(&mut self) -> BeatRateWidget { - BeatRateWidget { - state: &mut self.state, - beat_rate: &mut self.data.beat_rate, - } - } - - pub fn time_signature_widget(&mut self) -> TimeSignatureWidget { - TimeSignatureWidget { - state: &mut self.state, - time_signatures: &mut self.data.time_signatures, - } - } - - pub fn lines_widget(&mut self, col: Color) -> LinesWidget { - LinesWidget { - state: &mut self.state, - lines: match col { - Color::Green => &mut self.data.green_lines, - Color::Yellow => &mut self.data.yellow_lines, - Color::Orange => &mut self.data.orange_lines, - }, - color: col, - } - } - - pub fn waveform_widget<'a>(&'a mut self, song: &'a Song) -> WaveformWidget { - WaveformWidget { - state: &mut self.state, - song, - } - } -} - -impl<'a> egui::Widget for BeatRateWidget<'a> { - fn ui(self, ui: &mut egui::Ui) -> egui::Response { - let (rect, res) = allocate_editor_space(ui); - // handle interactions - // draw widget - if ui.is_rect_visible(rect) { - 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<'a> egui::Widget for LinesWidget<'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<'a> egui::Widget for WaveformWidget<'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, - msg_queue: VecDeque::new(), - level_list: gd::OuterLevel::load_all(), - loaded_song: None, - loaded_level_checksum: None, - error: None, - editor: Editor { - state: EditorState { - scroll_pos: 0f32, - pts_per_second: 5f32, - subdivisions: 4.0, - }, - data: GdlData { - 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| { - ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { - if ui - .add_enabled( - self.selected_level.is_some(), - egui::Button::new("Load Level"), - ) - .clicked() - { - self.msg_queue.push_back(Message::LoadLevel); - } - 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) { - egui::CentralPanel::default().show(ctx, |ui| { - ui.vertical_centered_justified(|ui| { - ui.label( - egui::RichText::new(match &self.loaded_song { - Some(song) => &song.name, - None => "No song loaded...", - }) - .size(32.0), - ); - ui.label( - egui::RichText::new(match &self.loaded_song { - Some(song) => song.id.to_string(), - None => "No song loaded...".into(), - }) - .size(20.0), - ); - - if let Some(song) = &self.loaded_song { - ui.add(self.editor.time_signature_widget()); - ui.add(self.editor.beat_rate_widget()); - ui.add(self.editor.lines_widget(Color::Green)); - ui.add(self.editor.lines_widget(Color::Orange)); - ui.add(self.editor.lines_widget(Color::Yellow)); - ui.add(self.editor.waveform_widget(song)); - } - }); - }); - } - - fn handle_message(&mut self, message: Message) { - match message { - Message::LevelSelected(idx) => self.selected_level = Some(idx), - Message::CloseError => self.error = None, - Message::LoadLevel => { - // Load song & GdlData - let level = self - .selected_level - .and_then(|idx| self.level_list.get(idx)) - .unwrap() // will not panic. selected_level range is same as level_list... - .clone(); // ...length - 1; message will not be sent if selected_level is none - - todo!(); - } - } - } - - fn handle_messages(&mut self) { - for message in mem::take(&mut self.msg_queue) { - println!("{message:?}"); - self.handle_message(message); - } - } -} - -impl eframe::App for PipeDash { - fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { - ctx.set_pixels_per_point(2f32); - - if let Some(boxed_err) = &self.error { - egui::CentralPanel::default().show(ctx, |ui| { - ui.label(boxed_err.to_string()); - if ui.button("Close").clicked() { - self.msg_queue.push_back(Message::CloseError); - } - }); - } else { - self.side_panel(ctx, frame); - self.center_panel(ctx, frame); - } - - self.handle_messages(); - } -} - -fn main() { - let app: PipeDash; - let opts = eframe::NativeOptions::default(); - eframe::run_native("PipeDash", opts, Box::new(|cc| Box::new(PipeDash::new(cc)))); -} diff --git a/src/gd.rs b/src/gd.rs index 8fbdf55..4baec54 100644 --- a/src/gd.rs +++ b/src/gd.rs @@ -13,24 +13,21 @@ use std::num::ParseIntError; use std::path::PathBuf; use thiserror::Error; +#[derive(Clone, Copy, Debug)] pub enum Song { - Official { id: i32 /*k8*/ }, - Newgrounds { id: i32 /*k45*/ }, + Official { id: i64 /*k8*/ }, + Newgrounds { id: i64 /*k45*/ }, Unknown, } #[derive(Clone)] pub struct SongResponse([Option; 9]); -struct Level { - outer: OuterLevel, - song: Song, -} - #[derive(Debug, Clone)] -pub struct OuterLevel { +pub struct Level { name: String, // k2 revision: Option, // k46 + song: Song, // k8 or k45 } #[derive(Debug)] @@ -160,7 +157,7 @@ impl SongResponse { } } -impl OuterLevel { +impl Level { pub fn load_all() -> Vec { get_local_level_plist() .as_dictionary() @@ -179,7 +176,13 @@ impl OuterLevel { if let Some(rev) = props.get("k46") { builder.with_revision(rev.as_signed_integer().unwrap()); } - builder.build_outer_level().unwrap() + if let Some(official_song) = props.get("k8") { + builder.with_song(Song::Official { id: official_song.as_signed_integer().unwrap() }); + } + if let Some(ng_song) = props.get("k45") { + builder.with_song(Song::Newgrounds { id: ng_song.as_signed_integer().unwrap() }); + } + builder.build_level().unwrap() }) .collect() } @@ -206,7 +209,7 @@ impl OuterLevel { .get("k4") .unwrap() .as_string() - .and_then(|str| InnerLevel::try_from_encoded_ils(str)) + .and_then(InnerLevel::try_from_encoded_ils) .unwrap() } @@ -229,6 +232,10 @@ impl OuterLevel { job } } + + pub fn song(&self) -> Song { + self.song + } } pub fn save_path() -> PathBuf { @@ -285,23 +292,9 @@ impl LevelBuilder { match self { Self { name: Some(name), - song: Some(song), revision, - } => Some(Level { song, - outer: OuterLevel { name, revision }, - }), - _ => None, - } - } - - fn build_outer_level(self) -> Option { - match self { - Self { - name: Some(name), - revision, - .. - } => Some(OuterLevel { name, revision }), + } => Some(Level { name, revision, song: song.unwrap_or(Song::Unknown)}), _ => None, } } diff --git a/src/main.rs b/src/main.rs index 8c89cdd..e4a02b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ #![feature(iter_array_chunks)] -#![warn(clippy::all, clippy::pedantic, clippy::nursery)] +//#![warn(clippy::all, clippy::pedantic, clippy::nursery)] #![allow(dead_code)] mod gd; @@ -22,11 +22,11 @@ fn project_dirs() -> directories::ProjectDirs { struct PipeDash { msg_queue: VecDeque, selected_level: Option, - level_list: Vec, + level_list: Vec, + loaded_level_checksum: Option<(gd::Level, md5::Digest)>, loaded_song: Option, - loaded_level_checksum: Option<(gd::OuterLevel, md5::Digest)>, editor: Editor, - error: Option>, + errors: VecDeque>, } #[derive(Debug, PartialEq, Eq)] @@ -43,11 +43,22 @@ enum Message { LoadLevel, } +enum EditorMode { + NoSong, + RhythmWizard {editor: WizardEditor, song: Song}, + Full {editor: Editor, song: Song} +} + struct Editor { state: EditorState, data: GdlData, } +struct WizardEditor { + state: EditorState, + data: WizardData, +} + struct EditorState { scroll_pos: f32, pts_per_second: f32, // zoom level @@ -62,9 +73,17 @@ struct GdlData { time_signatures: music::TimeSignature, } +struct WizardData { + green_lines: music::Lines, + orange_lines: music::Lines, + yellow_lines: music::Lines, + beat_rate: Option, + time_signatures: Option, +} + struct Song { name: String, - id: i32, + id: i64, decoder: rodio::Decoder, } @@ -260,10 +279,10 @@ impl PipeDash { Self { selected_level: None, msg_queue: VecDeque::new(), - level_list: gd::OuterLevel::load_all(), + level_list: gd::Level::load_all(), loaded_song: None, loaded_level_checksum: None, - error: None, + errors: VecDeque::new(), editor: Editor { state: EditorState { scroll_pos: 0f32, @@ -347,15 +366,34 @@ impl PipeDash { fn handle_message(&mut self, message: Message) { match message { Message::LevelSelected(idx) => self.selected_level = Some(idx), - Message::CloseError => self.error = None, + Message::CloseError => { self.errors.pop_front(); }, Message::LoadLevel => { - // Load song & GdlData + // Load song, GdlData, checksum; if there are no lines go straight into editor, otherwise + // do some sort of rhythm wizard thing + // [X] Load song + // [ ] Load GdlData + // [X] Load checksum + // [ ] Handle rhythm wizard let level = self .selected_level .and_then(|idx| self.level_list.get(idx)) .unwrap() // will not panic. selected_level range is same as level_list... .clone(); // ...length - 1; message will not be sent if selected_level is none + match Song::try_new(&level.song()) { + Ok(song) => { + self.loaded_song = Some(song); + }, + Err(e) => { + self.errors.push_front(Box::new(e)); + return; + }, + } + + let inner_level = level.load_inner(); + let lines = inner_level.get_lines(); + + self.loaded_level_checksum = Some((level, inner_level.hash())); todo!(); } } @@ -373,7 +411,7 @@ impl eframe::App for PipeDash { fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { ctx.set_pixels_per_point(2f32); - if let Some(boxed_err) = &self.error { + if let Some(boxed_err) = &self.errors.front() { egui::CentralPanel::default().show(ctx, |ui| { ui.label(boxed_err.to_string()); if ui.button("Close").clicked() {