bms_rs/chart_process/
bmson_processor.rs

1//! Bmson Processor Module.
2
3use std::collections::{BTreeMap, HashMap};
4use std::path::Path;
5use std::time::SystemTime;
6
7use crate::bms::prelude::*;
8use crate::bmson::prelude::*;
9use crate::chart_process::{
10    ChartEvent, ChartEventWithPosition, ChartProcessor, ControlEvent, VisibleEvent,
11    types::{BmpId, DisplayRatio, WavId, YCoordinate},
12};
13use std::str::FromStr;
14
15/// ChartProcessor of Bmson files.
16pub struct BmsonProcessor<'a> {
17    bmson: Bmson<'a>,
18
19    // Resource ID mappings
20    /// Audio filename to WavId mapping
21    audio_name_to_id: HashMap<String, WavId>,
22    /// Image filename to BmpId mapping
23    bmp_name_to_id: HashMap<String, BmpId>,
24
25    // Playback state
26    started_at: Option<SystemTime>,
27    last_poll_at: Option<SystemTime>,
28    progressed_y: Decimal,
29
30    // Flow parameters
31    default_visible_y_length: YCoordinate,
32    current_bpm: Decimal,
33    current_scroll: Decimal,
34
35    /// Pending external events queue
36    inbox: Vec<ControlEvent>,
37
38    /// Preloaded events list (all events in current visible area)
39    preloaded_events: Vec<ChartEventWithPosition>,
40
41    /// Preprocessed all events mapping, sorted by y coordinate
42    all_events: BTreeMap<YCoordinate, Vec<ChartEvent>>,
43}
44
45impl<'a> BmsonProcessor<'a> {
46    /// Create BMSON processor and initialize playback state with default parameters.
47    #[must_use]
48    pub fn new(bmson: Bmson<'a>) -> Self {
49        let init_bpm: Decimal = bmson.info.init_bpm.as_f64().into();
50
51        // Preprocessing: assign IDs to all audio and image resources
52        let mut audio_name_to_id = HashMap::new();
53        let mut bmp_name_to_id = HashMap::new();
54        let mut next_audio_id = 0usize;
55        let mut next_bmp_id = 0usize;
56
57        // Process audio files
58        for sound_channel in &bmson.sound_channels {
59            let std::collections::hash_map::Entry::Vacant(e) =
60                audio_name_to_id.entry(sound_channel.name.to_string())
61            else {
62                continue;
63            };
64            e.insert(WavId::new(next_audio_id));
65            next_audio_id += 1;
66        }
67
68        // Process mine audio files
69        for mine_channel in &bmson.mine_channels {
70            let std::collections::hash_map::Entry::Vacant(e) =
71                audio_name_to_id.entry(mine_channel.name.to_string())
72            else {
73                continue;
74            };
75            e.insert(WavId::new(next_audio_id));
76            next_audio_id += 1;
77        }
78
79        // Process hidden key audio files
80        for key_channel in &bmson.key_channels {
81            let std::collections::hash_map::Entry::Vacant(e) =
82                audio_name_to_id.entry(key_channel.name.to_string())
83            else {
84                continue;
85            };
86            e.insert(WavId::new(next_audio_id));
87            next_audio_id += 1;
88        }
89
90        // Process image files
91        for BgaHeader { name, .. } in &bmson.bga.bga_header {
92            let std::collections::hash_map::Entry::Vacant(e) =
93                bmp_name_to_id.entry(name.to_string())
94            else {
95                continue;
96            };
97            e.insert(BmpId::new(next_bmp_id));
98            next_bmp_id += 1;
99        }
100
101        // Calculate visible Y length based on starting BPM and 600ms reaction time
102        // Formula: visible Y length = (BPM / 120.0) * 0.6 seconds
103        // Where 0.6 seconds = 600ms, 120.0 is the base BPM
104        let reaction_time_seconds = Decimal::from_str("0.6").unwrap(); // 600ms
105        let base_bpm = Decimal::from(120);
106        let visible_y_length = (init_bpm.clone() / base_bpm) * reaction_time_seconds;
107
108        let mut processor = Self {
109            bmson,
110            audio_name_to_id,
111            bmp_name_to_id,
112            started_at: None,
113            last_poll_at: None,
114            progressed_y: Decimal::from(0),
115            inbox: Vec::new(),
116            preloaded_events: Vec::new(),
117            all_events: BTreeMap::new(),
118            default_visible_y_length: YCoordinate::from(visible_y_length),
119            current_bpm: init_bpm,
120            current_scroll: Decimal::from(1),
121        };
122
123        processor.preprocess_events();
124        processor
125    }
126
127    /// Preprocess all events, create event mapping sorted by y coordinate
128    fn preprocess_events(&mut self) {
129        let mut events_map: BTreeMap<YCoordinate, Vec<ChartEvent>> = BTreeMap::new();
130
131        // Process sound channel events
132        for SoundChannel { name, notes } in &self.bmson.sound_channels {
133            for Note { y, x, l, c, .. } in notes {
134                let yy = self.pulses_to_y(y.0);
135                let y_coord = YCoordinate::from(yy.clone());
136
137                let Some((side, key)) = Self::lane_from_x(x.as_ref().copied()) else {
138                    let wav_id = self.get_wav_id_for_name(name);
139                    let event = ChartEvent::Bgm { wav_id };
140                    events_map.entry(y_coord).or_default().push(event);
141                    continue;
142                };
143                let wav_id = self.get_wav_id_for_name(name);
144                let length = (*l > 0).then(|| {
145                    let end_y = self.pulses_to_y(y.0 + l);
146                    YCoordinate::from(end_y - yy.clone())
147                });
148                let kind = if *l > 0 {
149                    NoteKind::Long
150                } else {
151                    NoteKind::Visible
152                };
153                let event = ChartEvent::Note {
154                    side,
155                    key,
156                    kind,
157                    wav_id,
158                    length,
159                    continue_play: *c,
160                };
161                events_map.entry(y_coord).or_default().push(event);
162            }
163        }
164
165        // Process BPM events
166        for ev in &self.bmson.bpm_events {
167            let y = self.pulses_to_y(ev.y.0);
168            let y_coord = YCoordinate::from(y);
169            let event = ChartEvent::BpmChange {
170                bpm: ev.bpm.as_f64().into(),
171            };
172            events_map.entry(y_coord).or_default().push(event);
173        }
174
175        // Process Scroll events
176        for ScrollEvent { y, rate } in &self.bmson.scroll_events {
177            let y = self.pulses_to_y(y.0);
178            let y_coord = YCoordinate::from(y);
179            let event = ChartEvent::ScrollChange {
180                factor: rate.as_f64().into(),
181            };
182            events_map.entry(y_coord).or_default().push(event);
183        }
184
185        // Process Stop events
186        for stop in &self.bmson.stop_events {
187            let y = self.pulses_to_y(stop.y.0);
188            let y_coord = YCoordinate::from(y);
189            let event = ChartEvent::Stop {
190                duration: (stop.duration as f64).into(),
191            };
192            events_map.entry(y_coord).or_default().push(event);
193        }
194
195        // Process BGA base layer events
196        for BgaEvent { y, id, .. } in &self.bmson.bga.bga_events {
197            let yy = self.pulses_to_y(y.0);
198            let y_coord = YCoordinate::from(yy);
199            let bmp_name = self
200                .bmson
201                .bga
202                .bga_header
203                .iter()
204                .find(|header| header.id.0 == id.0)
205                .map(|header| &*header.name);
206            let bmp_id = bmp_name.and_then(|name| self.get_bmp_id_for_name(name));
207            let event = ChartEvent::BgaChange {
208                layer: BgaLayer::Base,
209                bmp_id,
210            };
211            events_map.entry(y_coord).or_default().push(event);
212        }
213
214        // Process BGA overlay layer events
215        for BgaEvent { y, id, .. } in &self.bmson.bga.layer_events {
216            let yy = self.pulses_to_y(y.0);
217            let y_coord = YCoordinate::from(yy);
218            let bmp_name = self
219                .bmson
220                .bga
221                .bga_header
222                .iter()
223                .find(|header| header.id.0 == id.0)
224                .map(|header| &*header.name);
225            let bmp_id = bmp_name.and_then(|name| self.get_bmp_id_for_name(name));
226            let event = ChartEvent::BgaChange {
227                layer: BgaLayer::Overlay,
228                bmp_id,
229            };
230            events_map.entry(y_coord).or_default().push(event);
231        }
232
233        // Process BGA poor layer events
234        for BgaEvent { y, id, .. } in &self.bmson.bga.poor_events {
235            let yy = self.pulses_to_y(y.0);
236            let y_coord = YCoordinate::from(yy);
237            let bmp_name = self
238                .bmson
239                .bga
240                .bga_header
241                .iter()
242                .find(|header| header.id.0 == id.0)
243                .map(|header| &*header.name);
244            let bmp_id = bmp_name.and_then(|name| self.get_bmp_id_for_name(name));
245            let event = ChartEvent::BgaChange {
246                layer: BgaLayer::Poor,
247                bmp_id,
248            };
249            events_map.entry(y_coord).or_default().push(event);
250        }
251
252        // Process bar line events - generated last but not exceeding other objects
253        if let Some(lines) = &self.bmson.lines {
254            for bar_line in lines {
255                let y = self.pulses_to_y(bar_line.y.0);
256                let y_coord = YCoordinate::from(y);
257                let event = ChartEvent::BarLine;
258                events_map.entry(y_coord).or_default().push(event);
259            }
260        } else {
261            // If barline is not defined, generate measure lines at each unit Y value, but not exceeding other objects' Y values
262            self.generate_auto_barlines(&mut events_map);
263        }
264
265        // Process mine channel events
266        for MineChannel { name, notes } in &self.bmson.mine_channels {
267            for MineEvent { x, y, .. } in notes {
268                let yy = self.pulses_to_y(y.0);
269                let y_coord = YCoordinate::from(yy);
270                let Some((side, key)) = Self::lane_from_x(*x) else {
271                    continue;
272                };
273                let wav_id = self.get_wav_id_for_name(name);
274                let event = ChartEvent::Note {
275                    side,
276                    key,
277                    kind: NoteKind::Landmine,
278                    wav_id,
279                    length: None,
280                    continue_play: false,
281                };
282                events_map.entry(y_coord).or_default().push(event);
283            }
284        }
285
286        // Process hidden key channel events
287        for KeyChannel { name, notes } in &self.bmson.key_channels {
288            for KeyEvent { x, y, .. } in notes {
289                let yy = self.pulses_to_y(y.0);
290                let y_coord = YCoordinate::from(yy);
291                let Some((side, key)) = Self::lane_from_x(*x) else {
292                    continue;
293                };
294                let wav_id = self.get_wav_id_for_name(name);
295                let event = ChartEvent::Note {
296                    side,
297                    key,
298                    kind: NoteKind::Invisible,
299                    wav_id,
300                    length: None,
301                    continue_play: false,
302                };
303                events_map.entry(y_coord).or_default().push(event);
304            }
305        }
306
307        self.all_events = events_map;
308    }
309
310    /// Convert pulse count to unified y coordinate (unit: measure). One measure = 4*resolution pulses.
311    fn pulses_to_y(&self, pulses: u64) -> Decimal {
312        let denom = Decimal::from(4 * self.bmson.info.resolution.get());
313        if denom == Decimal::from(0) {
314            Decimal::from(0)
315        } else {
316            Decimal::from(pulses) / denom
317        }
318    }
319
320    /// Automatically generate measure lines for BMSON without defined barline (at each unit Y value, but not exceeding other objects' Y values)
321    fn generate_auto_barlines(&self, events_map: &mut BTreeMap<YCoordinate, Vec<ChartEvent>>) {
322        // Find the maximum Y value of all events
323        let max_y = events_map
324            .keys()
325            .map(|y_coord| y_coord.value())
326            .max()
327            .cloned()
328            .unwrap_or_else(|| Decimal::from(0));
329
330        if max_y <= Decimal::from(0) {
331            return;
332        }
333
334        // Generate measure lines at each unit Y value, but not exceeding maximum Y value
335        let mut current_y = Decimal::from(0);
336        while current_y <= max_y {
337            let y_coord = YCoordinate::from(current_y.clone());
338            let event = ChartEvent::BarLine;
339            events_map.entry(y_coord).or_default().push(event);
340            current_y += Decimal::from(1);
341        }
342    }
343
344    /// Get WavId for audio filename
345    fn get_wav_id_for_name(&self, name: &str) -> Option<WavId> {
346        self.audio_name_to_id.get(name).copied()
347    }
348
349    /// Get BmpId for image filename
350    fn get_bmp_id_for_name(&self, name: &str) -> Option<BmpId> {
351        self.bmp_name_to_id.get(name).copied()
352    }
353
354    /// Current instantaneous displacement velocity (y units per second).
355    /// y is the normalized measure unit: `y = pulses / (4*resolution)`, one measure equals 1 in default 4/4.
356    /// Model: v = (current_bpm / 120.0) (using fixed base BPM 120)
357    /// Note: BPM only affects y progression speed, does not change event positions; Scroll only affects display positions.
358    fn current_velocity(&self) -> Decimal {
359        let base_bpm = Decimal::from(120);
360        if self.current_bpm.is_sign_negative() {
361            Decimal::from(0)
362        } else {
363            (self.current_bpm.clone() / base_bpm).max(Decimal::from(0))
364        }
365    }
366
367    /// Get the next event that affects speed (sorted by y ascending): BPM/SCROLL.
368    fn next_flow_event_after(&self, y_from_exclusive: Decimal) -> Option<(Decimal, FlowEvent)> {
369        let mut best: Option<(Decimal, FlowEvent)> = None;
370
371        for ev in &self.bmson.bpm_events {
372            let y = self.pulses_to_y(ev.y.0);
373            if y > y_from_exclusive {
374                best = min_by_y_decimal(best, (y, FlowEvent::Bpm(ev.bpm.as_f64().into())));
375            }
376        }
377        for ScrollEvent { y, rate } in &self.bmson.scroll_events {
378            let y = self.pulses_to_y(y.0);
379            if y > y_from_exclusive {
380                best = min_by_y_decimal(best, (y, FlowEvent::Scroll(rate.as_f64().into())));
381            }
382        }
383        best
384    }
385
386    fn step_to(&mut self, now: SystemTime) {
387        let Some(started) = self.started_at else {
388            return;
389        };
390        let last = self.last_poll_at.unwrap_or(started);
391        if now.duration_since(last).unwrap_or_default().is_zero() {
392            return;
393        }
394
395        let mut remaining_secs =
396            Decimal::from(now.duration_since(last).unwrap_or_default().as_secs_f64());
397        let mut cur_vel = self.current_velocity();
398        let mut cur_y = self.progressed_y.clone();
399        loop {
400            let next_event = self.next_flow_event_after(cur_y.clone());
401            if next_event.is_none()
402                || cur_vel == Decimal::from(0)
403                || remaining_secs == Decimal::from(0)
404            {
405                cur_y += cur_vel * remaining_secs;
406                break;
407            }
408            let (event_y, evt) = next_event.unwrap();
409            if event_y.clone() <= cur_y.clone() {
410                self.apply_flow_event(evt);
411                cur_vel = self.current_velocity();
412                continue;
413            }
414            let distance = event_y.clone() - cur_y.clone();
415            if cur_vel > Decimal::from(0) {
416                let time_to_event_secs = distance / cur_vel.clone();
417                if time_to_event_secs <= remaining_secs {
418                    cur_y = event_y;
419                    remaining_secs -= time_to_event_secs;
420                    self.apply_flow_event(evt);
421                    cur_vel = self.current_velocity();
422                    continue;
423                }
424            }
425            cur_y += cur_vel * remaining_secs;
426            break;
427        }
428
429        self.progressed_y = cur_y;
430        self.last_poll_at = Some(now);
431    }
432
433    fn apply_flow_event(&mut self, evt: FlowEvent) {
434        match evt {
435            FlowEvent::Bpm(bpm) => self.current_bpm = Decimal::from(bpm),
436            FlowEvent::Scroll(s) => self.current_scroll = Decimal::from(s),
437        }
438    }
439
440    fn visible_window_y(&self) -> Decimal {
441        // Dynamically calculate visible window length based on current BPM and 600ms reaction time
442        // Formula: visible Y length = (current BPM / 120.0) * 0.6 seconds
443        let reaction_time_seconds = Decimal::from_str("0.6").unwrap(); // 600ms
444        let base_bpm = Decimal::from(120);
445        (self.current_bpm.clone() / base_bpm) * reaction_time_seconds
446    }
447
448    fn lane_from_x(x: Option<std::num::NonZeroU8>) -> Option<(PlayerSide, Key)> {
449        let lane_value = x.map_or(0, |l| l.get());
450        let (adjusted_lane, side) = if lane_value > 8 {
451            (lane_value - 8, PlayerSide::Player2)
452        } else {
453            (lane_value, PlayerSide::Player1)
454        };
455        let key = match adjusted_lane {
456            1..=7 => Key::Key(adjusted_lane),
457            8 => Key::Scratch(1),
458            _ => return None,
459        };
460        Some((side, key))
461    }
462}
463
464impl<'a> ChartProcessor for BmsonProcessor<'a> {
465    fn audio_files(&self) -> HashMap<WavId, &Path> {
466        // Note: Audio file paths in BMSON are relative to the chart file, here returning virtual paths
467        // When actually used, these paths need to be resolved based on the chart file location
468        self.audio_name_to_id
469            .iter()
470            .map(|(name, id)| (*id, Path::new(name)))
471            .collect()
472    }
473
474    fn bmp_files(&self) -> HashMap<BmpId, &Path> {
475        // Note: Image file paths in BMSON are relative to the chart file, here returning virtual paths
476        // When actually used, these paths need to be resolved based on the chart file location
477        self.bmp_name_to_id
478            .iter()
479            .map(|(name, id)| (*id, Path::new(name)))
480            .collect()
481    }
482
483    fn default_visible_y_length(&self) -> YCoordinate {
484        self.default_visible_y_length.clone()
485    }
486
487    fn current_bpm(&self) -> Decimal {
488        self.current_bpm.clone()
489    }
490    fn current_speed(&self) -> Decimal {
491        Decimal::from(1)
492    }
493    fn current_scroll(&self) -> Decimal {
494        self.current_scroll.clone()
495    }
496
497    fn start_play(&mut self, now: SystemTime) {
498        self.started_at = Some(now);
499        self.last_poll_at = Some(now);
500        self.progressed_y = Decimal::from(0);
501        self.preloaded_events.clear();
502        self.current_bpm = self.bmson.info.init_bpm.as_f64().into();
503    }
504
505    fn update(&mut self, now: SystemTime) -> impl Iterator<Item = ChartEventWithPosition> {
506        let incoming = std::mem::take(&mut self.inbox);
507        for evt in &incoming {
508            match evt {
509                ControlEvent::SetDefaultVisibleYLength { length } => {
510                    self.default_visible_y_length = length.clone();
511                }
512            }
513        }
514
515        let prev_y = self.progressed_y.clone();
516        self.step_to(now);
517        let cur_y = self.progressed_y.clone();
518
519        // Calculate preload range: current y + visible y range
520        let visible_y_length = self.visible_window_y();
521        let preload_end_y = cur_y.clone() + visible_y_length;
522
523        // Collect events triggered at current moment
524        let mut triggered_events: Vec<ChartEventWithPosition> = Vec::new();
525
526        // Collect events within preload range
527        let mut new_preloaded_events: Vec<ChartEventWithPosition> = Vec::new();
528
529        // Get events from preprocessed event mapping
530        for (y_coord, events) in &self.all_events {
531            let y_value = y_coord.value();
532
533            // Check if it's an event triggered at current moment
534            if *y_value > prev_y && *y_value <= cur_y {
535                for event in events {
536                    triggered_events
537                        .push(ChartEventWithPosition::new(y_coord.clone(), event.clone()));
538                }
539            }
540
541            // Check if it's an event within preload range
542            if *y_value > cur_y && *y_value <= preload_end_y {
543                for event in events {
544                    new_preloaded_events
545                        .push(ChartEventWithPosition::new(y_coord.clone(), event.clone()));
546                }
547            }
548        }
549
550        // Update preloaded events list
551        self.preloaded_events = new_preloaded_events;
552
553        triggered_events.into_iter()
554    }
555
556    fn post_events(&mut self, events: &[ControlEvent]) {
557        self.inbox.extend_from_slice(events);
558    }
559
560    fn visible_events(&mut self, now: SystemTime) -> impl Iterator<Item = VisibleEvent> {
561        self.step_to(now);
562        let current_y = self.progressed_y.clone();
563        let visible_window_y = self.visible_window_y();
564        let scroll_factor = self.current_scroll.clone();
565
566        self.preloaded_events.iter().map(move |event_with_pos| {
567            let event_y = event_with_pos.position().value();
568            // Calculate display ratio: (event_y - current_y) / visible_window_y * scroll_factor
569            // Note: scroll can be non-zero positive or negative values
570            let display_ratio_value = if visible_window_y > Decimal::from(0) {
571                ((event_y.clone() - current_y.clone()) / visible_window_y.clone())
572                    * scroll_factor.clone()
573            } else {
574                Decimal::from(0)
575            };
576            let display_ratio = DisplayRatio::from(display_ratio_value);
577            VisibleEvent::new(
578                event_with_pos.position().clone(),
579                event_with_pos.event().clone(),
580                display_ratio,
581            )
582        })
583    }
584}
585
586#[derive(Debug, Clone)]
587enum FlowEvent {
588    Bpm(Decimal),
589    Scroll(Decimal),
590}
591
592fn min_by_y_decimal(
593    best: Option<(Decimal, FlowEvent)>,
594    candidate: (Decimal, FlowEvent),
595) -> Option<(Decimal, FlowEvent)> {
596    match best {
597        None => Some(candidate),
598        Some((y, _)) if candidate.0 < y => Some(candidate),
599        Some(o) => Some(o),
600    }
601}