bms-rs 1.0.0

The BMS format parser.
Documentation
//! This module handles the tokens:
//!
//! - `#STOP[01-ZZ] n` - Stop definition. It stops the scroll as `n` of 192nd note.
//! - `#xxx09:` - Stop channel.
//! - `#STP xxx.yyy time` - It stops `time` milliseconds at section `xxx` and its position (`yyy` / 1000).

use std::{cell::RefCell, rc::Rc};

use super::{
    super::prompt::{DefDuplication, Prompter},
    ProcessContext, TokenProcessor, parse_obj_ids,
};
use crate::bms::ParseErrorWithRange;
use crate::{
    bms::{
        model::{StringValue, stop::StopObjects},
        parse::{ParseWarning, Result},
        prelude::*,
    },
    util::StrExtension,
};
use strict_num_extended::NonNegativeF64;

/// It processes `#STOPxx` definitions and objects on `Stop` channel.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StopProcessor {
    case_sensitive_obj_id: Rc<RefCell<bool>>,
}

impl StopProcessor {
    pub fn new(case_sensitive_obj_id: &Rc<RefCell<bool>>) -> Self {
        Self {
            case_sensitive_obj_id: Rc::clone(case_sensitive_obj_id),
        }
    }
}

impl TokenProcessor for StopProcessor {
    type Output = StopObjects;

    fn process<P: Prompter>(
        &self,
        ctx: &mut ProcessContext<'_, '_, P>,
    ) -> core::result::Result<Self::Output, ParseErrorWithRange> {
        let mut objects = StopObjects::default();
        ctx.all_tokens(|token, prompter| match token.content() {
            Token::Header { name, args } => Ok(self
                .on_header(name.as_ref(), args.as_ref(), prompter, &mut objects)
                .err()
                .map(|warn| warn.into_wrapper(token))),
            Token::Message {
                track,
                channel,
                message,
            } => Ok(self
                .on_message(
                    *track,
                    *channel,
                    &message.as_ref().into_wrapper(token),
                    &mut objects,
                    prompter,
                )
                .err()
                .map(|warn| warn.into_wrapper(token))),
            Token::NotACommand(_) => Ok(None),
        })?;
        Ok(objects)
    }
}

impl StopProcessor {
    fn on_header(
        &self,
        name: &str,
        args: &str,
        prompter: &impl Prompter,
        objects: &mut StopObjects,
    ) -> Result<()> {
        if let Some(id) = name.strip_prefix_ignore_case("STOP") {
            let string_value: StringValue<NonNegativeF64> = StringValue::new(args);

            let stop_obj_id = ObjId::try_from(id, *self.case_sensitive_obj_id.borrow())?;

            if let Some(older) = objects.stop_defs.get_mut(&stop_obj_id) {
                prompter
                    .handle_def_duplication(DefDuplication::Stop {
                        id: stop_obj_id,
                        older,
                        newer: &string_value,
                    })
                    .apply_def(older, string_value, stop_obj_id)?;
            } else {
                objects.stop_defs.insert(stop_obj_id, string_value);
            }
        }
        if name.eq_ignore_ascii_case("STP") {
            // Parse xxx.yyy zzzz
            use std::time::Duration;
            let args: Vec<_> = args.split_whitespace().collect();
            let [measure_pos, ms] = args.as_slice() else {
                return Err(ParseWarning::SyntaxError(
                    "stp requires 2 arguments: measure.position and milliseconds".into(),
                ));
            };

            let (measure, pos) = measure_pos.split_once('.').unwrap_or((measure_pos, "000"));
            let measure: u16 = measure
                .parse()
                .map_err(|_| ParseWarning::SyntaxError("expected measure u16".into()))?;
            let pos: u16 = pos
                .parse()
                .map_err(|_| ParseWarning::SyntaxError("expected pos u16".into()))?;
            let ms: u64 = ms
                .parse()
                .map_err(|_| ParseWarning::SyntaxError("expected pos u64".into()))?;
            let time = ObjTime::new(measure as u64, pos as u64, 1000).ok_or_else(|| {
                ParseWarning::SyntaxError("denominator should be non-zero".into())
            })?;
            let duration = Duration::from_millis(ms);

            // Store by ObjTime as key, handle duplication with prompt handler
            let ev = StpEvent { time, duration };
            if let Some(older) = objects.stp_events.get_mut(&time) {
                use crate::bms::parse::prompt::ChannelDuplication;

                prompter
                    .handle_channel_duplication(ChannelDuplication::StpEvent {
                        time,
                        older,
                        newer: &ev,
                    })
                    .apply_channel(older, ev, time, Channel::Stop)?;
            } else {
                objects.stp_events.insert(time, ev);
            }
        }
        Ok(())
    }

    fn on_message(
        &self,
        track: Track,
        channel: Channel,
        message: &SourceRangeMixin<&str>,
        objects: &mut StopObjects,
        prompter: &impl Prompter,
    ) -> core::result::Result<Vec<ParseWarningWithRange>, ParseWarning> {
        let mut warnings: Vec<ParseWarningWithRange> = Vec::new();
        if channel == Channel::Stop {
            let (pairs, w) = parse_obj_ids(track, message, &self.case_sensitive_obj_id);
            warnings.extend(w);
            for (time, obj) in pairs {
                // Record used STOP id for validity checks
                objects.stop_ids_used.insert(obj);
                let string_value = objects
                    .stop_defs
                    .get(&obj)
                    .ok_or(ParseWarning::UndefinedObject(obj))?;

                // Try to get the duration; if parsing failed, skip this object
                let Ok(&duration) = string_value.value().as_ref() else {
                    continue;
                };

                objects.push_stop(StopObj { time, duration }, prompter)?;
            }
        }
        Ok(warnings)
    }
}