diff --git a/.gitignore b/.gitignore index 7b071798..b5cea9b6 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ inner.rs /test /log* /build_event_debug.sh +/.idea diff --git a/prpr/src/core.rs b/prpr/src/core.rs index 7f9d4dbf..65550d80 100644 --- a/prpr/src/core.rs +++ b/prpr/src/core.rs @@ -123,6 +123,14 @@ impl BpmList { BpmList { elements, cursor: 0 } } + pub fn new_time(ranges: Vec<(f32, f32)>) -> Self { + let mut elements = Vec::new(); + for (time, bpm) in ranges { + elements.push((time, time, bpm)); + } + BpmList { elements, cursor: 0 } + } + /// Get the time in seconds for a given beats pub fn time_beats(&mut self, beats: f32) -> f32 { while let Some(kf) = self.elements.get(self.cursor + 1) { @@ -157,4 +165,18 @@ impl BpmList { let (beats, start_time, bpm) = &self.elements[self.cursor]; beats + (time - start_time) / (60. / bpm) } + + pub fn now_bpm(&mut self, time: f32) -> f32 { + while let Some(kf) = self.elements.get(self.cursor + 1) { + if kf.1 > time { + break; + } + self.cursor += 1; + } + while self.cursor != 0 && self.elements[self.cursor].1 > time { + self.cursor -= 1; + } + let (_, _, bpm) = &self.elements[self.cursor]; + *bpm + } } diff --git a/prpr/src/core/chart.rs b/prpr/src/core/chart.rs index bb9032d1..29bd22f9 100644 --- a/prpr/src/core/chart.rs +++ b/prpr/src/core/chart.rs @@ -106,9 +106,11 @@ impl Chart { } // TODO optimize let trs = self.lines.iter().map(|it| it.now_transform(res, &self.lines)).collect::>(); - for (line, tr) in self.lines.iter_mut().zip(trs) { - line.update(res, tr); + let mut guard = self.bpm_list.borrow_mut(); + for (index, (line, tr)) in self.lines.iter_mut().zip(trs).enumerate() { + line.update(res, tr, &mut guard, index); } + drop(guard); for effect in &mut self.extra.effects { effect.update(res); } diff --git a/prpr/src/core/line.rs b/prpr/src/core/line.rs index 88b9bfc9..b7833673 100644 --- a/prpr/src/core/line.rs +++ b/prpr/src/core/line.rs @@ -157,7 +157,7 @@ pub struct JudgeLine { } impl JudgeLine { - pub fn update(&mut self, res: &mut Resource, tr: Matrix) { + pub fn update(&mut self, res: &mut Resource, tr: Matrix, bpm_list: &mut BpmList, index: usize) { // self.object.set_time(res.time); // this is done by chart, chart has to calculate transform for us let rot = self.object.rotation.now(); self.height.set_time(res.time); @@ -165,7 +165,7 @@ impl JudgeLine { let mut ctrl_obj = self.ctrl_obj.borrow_mut(); self.cache.update_order.retain(|id| { let note = &mut self.notes[*id as usize]; - note.update(res, rot, &tr, &mut ctrl_obj, line_height); + note.update(res, rot, &tr, &mut ctrl_obj, line_height, bpm_list, index); !note.dead() }); drop(ctrl_obj); diff --git a/prpr/src/core/note.rs b/prpr/src/core/note.rs index fcdb6a85..97ba3492 100644 --- a/prpr/src/core/note.rs +++ b/prpr/src/core/note.rs @@ -2,10 +2,12 @@ use super::{chart::ChartSettings, BpmList, CtrlObject, JudgeLine, Matrix, Object pub use crate::{ judge::{HitSound, JudgeStatus}, parse::RPE_HEIGHT, + core::HEIGHT_RATIO, + info::ChartFormat, }; use macroquad::prelude::*; -const HOLD_PARTICLE_INTERVAL: f32 = 0.15; +//const HOLD_PARTICLE_INTERVAL: f32 = 0.15; const FADEOUT_TIME: f32 = 0.16; const BAD_TIME: f32 = 0.5; @@ -134,11 +136,14 @@ impl Note { // && self.ctrl_obj.is_default() } - pub fn update(&mut self, res: &mut Resource, parent_rot: f32, parent_tr: &Matrix, ctrl_obj: &mut CtrlObject, line_height: f32) { + pub fn update(&mut self, res: &mut Resource, parent_rot: f32, parent_tr: &Matrix, ctrl_obj: &mut CtrlObject, line_height: f32, bpm_list: &mut BpmList, index: usize) { self.object.set_time(res.time); - if let Some(color) = if let JudgeStatus::Hold(perfect, at, ..) = &mut self.judge { - if res.time > *at { - *at += HOLD_PARTICLE_INTERVAL / res.config.speed; + if let Some(color) = if let JudgeStatus::Hold(perfect, ref mut at, ..) = &mut self.judge { + if res.time >= *at { + let beat = 30. / bpm_list.now_bpm( + if matches!(res.chart_format, ChartFormat::Pgr) { index as f32 } else { self.time } + ); + *at = res.time + beat / res.config.speed; Some(if *perfect { res.res_pack.info.fx_perfect() } else { @@ -200,7 +205,8 @@ impl Note { self.init_ctrl_obj(ctrl_obj, config.line_height); let mut color = self.object.now_color(); color.a *= res.alpha * ctrl_obj.alpha.now_opt().unwrap_or(1.); - let spd = self.speed * ctrl_obj.y.now_opt().unwrap_or(1.); + let end_spd = self.speed * ctrl_obj.y.now_opt().unwrap_or(1.); + let spd = if matches!(res.chart_format, ChartFormat::Pgr) && matches!(self.kind, NoteKind::Hold { .. }) { 1. } else { end_spd }; let line_height = config.line_height / res.aspect_ratio * spd; let height = self.height / res.aspect_ratio * spd; @@ -262,11 +268,23 @@ impl Note { let h = if self.time <= res.time { line_height } else { height }; let bottom = h - line_height; - let top = end_height - line_height; + let top = if matches!(res.chart_format, ChartFormat::Pgr) { + if end_spd == 0. { + return; + } + let time = if res.time >= self.time {res.time} else {self.time}; + let hold_height = end_height - height; + let hold_line_height = (time - self.time) * end_spd / res.aspect_ratio / HEIGHT_RATIO; + bottom + hold_height - hold_line_height + } else { + end_height - line_height + }; + if res.time < self.time && bottom < -1e-6 && !config.settings.hold_partial_cover && !matches!(res.chart_format, ChartFormat::Pgr) { + return; + } let tex = &style.hold; let ratio = style.hold_ratio(); // body - // TODO (end_height - height) is not always total height draw_tex( res, **(if res.res_pack.info.hold_repeat { diff --git a/prpr/src/core/resource.rs b/prpr/src/core/resource.rs index b3e4b84c..97ad35f4 100644 --- a/prpr/src/core/resource.rs +++ b/prpr/src/core/resource.rs @@ -18,6 +18,7 @@ use std::{ path::Path, sync::atomic::AtomicU32, }; +use crate::info::ChartFormat; pub const MAX_SIZE: usize = 64; // needs tweaking pub static DPI_VALUE: AtomicU32 = AtomicU32::new(250); @@ -349,6 +350,7 @@ pub type SfxMap = HashMap; pub struct Resource { pub config: Config, + pub chart_format: ChartFormat, pub info: ChartInfo, pub aspect_ratio: f32, pub dpi: u32, @@ -416,6 +418,7 @@ impl Resource { pub async fn new( config: Config, + chart_format: ChartFormat, info: ChartInfo, mut fs: Box, player: Option, @@ -456,6 +459,7 @@ impl Resource { macroquad::window::gl_set_drawcall_buffer_capacity(MAX_SIZE * 4, MAX_SIZE * 6); Ok(Self { config, + chart_format, info, aspect_ratio, dpi: DPI_VALUE.load(std::sync::atomic::Ordering::SeqCst), diff --git a/prpr/src/judge.rs b/prpr/src/judge.rs index 25ca3ef2..e47c57ce 100644 --- a/prpr/src/judge.rs +++ b/prpr/src/judge.rs @@ -853,7 +853,6 @@ impl Judge { } } for (line_id, id) in judgements.into_iter() { - self.commit(t, Judgement::Perfect, line_id as _, id, 0.); let (note_transform, note_hitsound) = { let line = &mut chart.lines[line_id]; let note = &mut line.notes[id as usize]; @@ -863,11 +862,14 @@ impl Judge { (note.object.now(res), note.hitsound.clone()) }; let line = &chart.lines[line_id]; - res.with_model(line.now_transform(res, &chart.lines) * note_transform, |res| { - res.emit_at_origin(line.notes[id as usize].rotation(&line), res.res_pack.info.fx_perfect()) - }); if !matches!(chart.lines[line_id].notes[id as usize].kind, NoteKind::Hold { .. }) { + self.commit(t, Judgement::Perfect, line_id as _, id, 0.); + res.with_model(line.now_transform(res, &chart.lines) * note_transform, |res| { + res.emit_at_origin(line.notes[id as usize].rotation(line), res.res_pack.info.fx_perfect()) + }); note_hitsound.play(res); + } else { + self.commit(t, Judgement::Perfect, line_id as _, id, 0.); } } } @@ -968,13 +970,13 @@ pub struct PlayResult { pub fn icon_index(score: u32, full_combo: bool) -> usize { match (score, full_combo) { + (1000000, true) => 7, + (_, true) => 6, (x, _) if x < 700000 => 0, (x, _) if x < 820000 => 1, (x, _) if x < 880000 => 2, (x, _) if x < 920000 => 3, (x, _) if x < 960000 => 4, - (1000000, _) => 7, (_, false) => 5, - (_, true) => 6, } } diff --git a/prpr/src/parse/pgr.rs b/prpr/src/parse/pgr.rs index 2b7a7118..107db30d 100644 --- a/prpr/src/parse/pgr.rs +++ b/prpr/src/parse/pgr.rs @@ -44,7 +44,7 @@ pub struct PgrNote { position_x: f32, hold_time: f32, speed: f32, - floor_position: f32, + #[allow(unused)]floor_position: f32, } #[derive(Deserialize)] @@ -66,6 +66,7 @@ struct PgrJudgeLine { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct PgrChart { + format_version: u32, offset: f32, judge_line_list: Vec, } @@ -80,11 +81,11 @@ macro_rules! validate_events { true } }); - for i in 0..($pgr.len() - 1) { - if $pgr[i].end_time != $pgr[i + 1].start_time { - ptl!(bail "event-not-contiguous"); - } - } + // for i in 0..($pgr.len() - 1) { + // if $pgr[i].end_time != $pgr[i + 1].start_time { + // ptl!(bail "event-not-contiguous"); + // } + // } // if $pgr.last().unwrap().end_time <= 900000000.0 { // bail!("End time is not great enough ({})", $pgr.last().unwrap().end_time); // } @@ -93,7 +94,7 @@ macro_rules! validate_events { fn parse_speed_events(r: f32, mut pgr: Vec, max_time: f32) -> Result<(AnimFloat, AnimFloat)> { validate_events!(pgr); - assert_eq!(pgr[0].start_time, 0.0); + pgr[0].start_time = 0.; let mut kfs = Vec::new(); let mut pos = 0.; kfs.extend(pgr[..pgr.len().saturating_sub(1)].iter().map(|it| { @@ -150,7 +151,38 @@ fn parse_move_events(r: f32, mut pgr: Vec) -> Result { Ok(AnimVector(AnimFloat::new(kf1), AnimFloat::new(kf2))) } -fn parse_notes(r: f32, mut pgr: Vec, speed: &mut AnimFloat, height: &mut AnimFloat, above: bool) -> Result> { +fn parse_move_events_fv1(r: f32, mut pgr: Vec) -> Result { + validate_events!(pgr); + let mut kf1 = Vec::>::new(); + let mut kf2 = Vec::>::new(); + for e in pgr { + let st = (e.start_time * r).max(0.); + let en = e.end_time * r; + if !kf1.last().map_or(false, |it| it.value == e.start) { + let start = (e.start - e.start % 1000.) / 1000.; + kf1.push(Keyframe::new(st, start, 2)); + } + if !kf2.last().map_or(false, |it| it.value == e.start2) { + let start2 = e.start % 1000.; + kf2.push(Keyframe::new(st, start2, 2)); + } + let end = (e.end - e.end % 1000.) / 1000.; + let end2 = e.end % 1000.; + kf1.push(Keyframe::new(en, end, 2)); + kf2.push(Keyframe::new(en, end2, 2)); + } + kf1.pop(); + kf2.pop(); + for kf in &mut kf1 { + kf.value = (-880. + kf.value * 2.) / 880.; + } + for kf in &mut kf2 { + kf.value = (-520. + kf.value * 2.) / 520.; + } + Ok(AnimVector(AnimFloat::new(kf1), AnimFloat::new(kf2))) +} + +fn parse_notes(r: f32, mut pgr: Vec, _speed: &mut AnimFloat, height: &mut AnimFloat, above: bool) -> Result> { // is_sorted is unstable... if pgr.is_empty() { return Ok(Vec::new()); @@ -159,13 +191,16 @@ fn parse_notes(r: f32, mut pgr: Vec, speed: &mut AnimFloat, height: &mu pgr.into_iter() .map(|pgr| { let time = pgr.time * r; + let height = { + height.set_time(time); + height.now() + }; let kind = match pgr.kind { 1 => NoteKind::Click, 2 => NoteKind::Drag, 3 => { let end_time = (pgr.time + pgr.hold_time) * r; - height.set_time(end_time); - let end_height = height.now(); + let end_height = height + (pgr.hold_time * pgr.speed * r / HEIGHT_RATIO); NoteKind::Hold { end_time, end_height } } 4 => NoteKind::Flick, @@ -180,13 +215,8 @@ fn parse_notes(r: f32, mut pgr: Vec, speed: &mut AnimFloat, height: &mu kind, hitsound, time, - speed: if pgr.kind == 3 { - speed.set_time(time); - pgr.speed / speed.now() - } else { - pgr.speed - }, - height: pgr.floor_position / HEIGHT_RATIO, + speed: pgr.speed, + height, above, multiple_hint: false, @@ -197,7 +227,7 @@ fn parse_notes(r: f32, mut pgr: Vec, speed: &mut AnimFloat, height: &mu .collect() } -fn parse_judge_line(pgr: PgrJudgeLine, max_time: f32) -> Result { +fn parse_judge_line(pgr: PgrJudgeLine, max_time: f32, format_version: u32) -> Result { let r = 60. / pgr.bpm / 32.; let (mut speed, mut height) = parse_speed_events(r, pgr.speed_events, max_time).context("Failed to parse speed events")?; let notes_above = parse_notes(r, pgr.notes_above, &mut speed, &mut height, true).context("Failed to parse notes above")?; @@ -209,7 +239,13 @@ fn parse_judge_line(pgr: PgrJudgeLine, max_time: f32) -> Result { object: Object { alpha: parse_float_events(r, pgr.alpha_events).with_context(|| ptl!("alpha-events-parse-failed"))?, rotation: parse_float_events(r, pgr.rotate_events).with_context(|| ptl!("rotate-events-parse-failed"))?, - translation: parse_move_events(r, pgr.move_events).with_context(|| ptl!("move-events-parse-failed"))?, + translation: { + match format_version { + 1 => parse_move_events_fv1(r, pgr.move_events).with_context(|| ptl!("move-events-parse-failed"))?, + 3 => parse_move_events(r, pgr.move_events).with_context(|| ptl!("move-events-parse-failed"))?, + _ => ptl!(bail "unknown-format-version"), + } + }, ..Default::default() }, ctrl_obj: RefCell::default(), @@ -220,7 +256,7 @@ fn parse_judge_line(pgr: PgrJudgeLine, max_time: f32) -> Result { color: Anim::default(), parent: None, z_index: 0, - show_below: true, + show_below: false, attach_ui: None, cache, @@ -229,6 +265,11 @@ fn parse_judge_line(pgr: PgrJudgeLine, max_time: f32) -> Result { pub fn parse_phigros(source: &str, extra: ChartExtra) -> Result { let pgr: PgrChart = serde_json::from_str(source).with_context(|| ptl!("json-parse-failed"))?; + let format_version = pgr.format_version; + let mut bpm_values = Vec::new(); + for (index, judge_line) in pgr.judge_line_list.iter().enumerate() { + bpm_values.push((index as f32, judge_line.bpm)); + } let max_time = *pgr .judge_line_list .iter() @@ -248,8 +289,8 @@ pub fn parse_phigros(source: &str, extra: ChartExtra) -> Result { .judge_line_list .into_iter() .enumerate() - .map(|(id, pgr)| parse_judge_line(pgr, max_time).with_context(|| ptl!("judge-line-location", "jlid" => id))) + .map(|(id, pgr)| parse_judge_line(pgr, max_time, format_version).with_context(|| ptl!("judge-line-location", "jlid" => id))) .collect::>>()?; process_lines(&mut lines); - Ok(Chart::new(pgr.offset, lines, BpmList::default(), ChartSettings::default(), extra, HashMap::new())) + Ok(Chart::new(pgr.offset, lines, BpmList::new_time(bpm_values), ChartSettings::default(), extra, HashMap::new())) } diff --git a/prpr/src/scene/game.rs b/prpr/src/scene/game.rs index 6044cb08..330a00e9 100644 --- a/prpr/src/scene/game.rs +++ b/prpr/src/scene/game.rs @@ -123,7 +123,6 @@ pub struct GameScene { pub gl: InternalGlContext<'static>, player: Option, chart_bytes: Vec, - chart_format: ChartFormat, info_offset: f32, effects: Vec, @@ -248,6 +247,7 @@ impl GameScene { let info_offset = info.offset; let mut res = Resource::new( config, + chart_format, info, fs, player.as_ref().and_then(|it| it.avatar.clone()), @@ -281,7 +281,6 @@ impl GameScene { gl: unsafe { get_internal_gl() }, player, chart_bytes, - chart_format, effects, info_offset, @@ -449,7 +448,7 @@ impl GameScene { let hw = 0.003; let height = eps * 1.2; - let dest = 2. * res.time / res.track_length; + let dest = (2. * res.time / res.track_length).max(0.).min(2.); ui.fill_rect(Rect::new(-1., top, dest, height), semi_white(0.6)); ui.fill_rect(Rect::new(-1. + dest - hw, top, hw * 2., height), WHITE); });