bms_rs/bms/command/
minor_command.rs

1#![cfg(feature = "minor-command")]
2
3use crate::bms::command::time::ObjTime;
4use std::time::Duration;
5
6use super::{ObjId, graphics::Argb};
7
8/// Pan value for `#EXWAV` sound effect.
9/// Range: \[-10000, 10000]. -10000 is leftmost, 10000 is rightmost.
10/// Default: 0.
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13pub struct ExWavPan(i64);
14
15impl ExWavPan {
16    /// Creates a new [`ExWavPan`] value.
17    /// Returns `None` if the value is out of range \[-10000, 10000].
18    #[must_use]
19    pub fn new(value: i64) -> Option<Self> {
20        (-10000..=10000).contains(&value).then_some(Self(value))
21    }
22
23    /// Returns the underlying value.
24    #[must_use]
25    pub fn value(self) -> i64 {
26        self.0
27    }
28
29    /// Returns the default value (0).
30    #[must_use]
31    pub const fn default() -> Self {
32        Self(0)
33    }
34}
35
36impl TryFrom<i64> for ExWavPan {
37    type Error = i64;
38
39    fn try_from(value: i64) -> std::result::Result<Self, Self::Error> {
40        Self::new(value).ok_or(value.clamp(-10000, 10000))
41    }
42}
43
44/// Volume value for `#EXWAV` sound effect.
45/// Range: \[-10000, 0]. -10000 is 0%, 0 is 100%.
46/// Default: 0.
47#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49pub struct ExWavVolume(i64);
50
51impl ExWavVolume {
52    /// Creates a new [`ExWavVolume`] value.
53    /// Returns `None` if the value is out of range `[-10000, 0]`.
54    #[must_use]
55    pub fn new(value: i64) -> Option<Self> {
56        (-10000..=0).contains(&value).then_some(Self(value))
57    }
58
59    /// Returns the underlying value.
60    #[must_use]
61    pub fn value(self) -> i64 {
62        self.0
63    }
64
65    /// Returns the default value (0).
66    #[must_use]
67    pub const fn default() -> Self {
68        Self(0)
69    }
70}
71
72impl TryFrom<i64> for ExWavVolume {
73    type Error = i64;
74
75    fn try_from(value: i64) -> std::result::Result<Self, Self::Error> {
76        Self::new(value).ok_or(value.clamp(-10000, 0))
77    }
78}
79
80/// Frequency value for `#EXWAV` sound effect.
81/// Range: \[100, 100000]. Unit: Hz.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
83#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
84pub struct ExWavFrequency(u64);
85
86impl ExWavFrequency {
87    const MIN_FREQUENCY: u64 = 100;
88    const MAX_FREQUENCY: u64 = 100_000;
89
90    /// Creates a new [`ExWavFrequency`] value.
91    /// Returns `None` if the value is out of range [100, 100000].
92    #[must_use]
93    pub fn new(value: u64) -> Option<Self> {
94        (Self::MIN_FREQUENCY..=Self::MAX_FREQUENCY)
95            .contains(&value)
96            .then_some(Self(value))
97    }
98
99    /// Returns the underlying value.
100    #[must_use]
101    pub fn value(self) -> u64 {
102        self.0
103    }
104}
105
106impl TryFrom<u64> for ExWavFrequency {
107    type Error = u64;
108
109    fn try_from(value: u64) -> std::result::Result<Self, Self::Error> {
110        Self::new(value).ok_or(value.clamp(Self::MIN_FREQUENCY, Self::MAX_FREQUENCY))
111    }
112}
113
114/// bemaniaDX type STP sequence definition.
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
116#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
117pub struct StpEvent {
118    /// The time of the stop.
119    pub time: ObjTime,
120    /// The duration of the stop.
121    pub duration: Duration,
122}
123
124/// MacBeat `#WAVCMD` event.
125///
126/// Used for `#WAVCMD` command, represents `pitch`/`volume`/`time` adjustment for a specific WAV object.
127/// - `param`: adjustment type (`pitch`/`volume`/`time`)
128/// - `wav_index`: target WAV object ID
129/// - `value`: adjustment value, meaning depends on param
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
131#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
132pub struct WavCmdEvent {
133    /// Adjustment type (pitch/volume/time)
134    pub param: WavCmdParam,
135    /// Target WAV object ID
136    pub wav_index: ObjId,
137    /// Adjustment value, meaning depends on param
138    pub value: u32,
139}
140
141/// SWBGA (Key Bind Layer Animation) event.
142///
143/// Used for `#SWBGA` command, describes key-bound BGA animation.
144/// - `frame_rate`: frame interval (ms), e.g. 60FPS=17
145/// - `total_time`: total animation duration (ms), 0 means while key is held
146/// - line: applicable key channel (e.g. 11-19, 21-29)
147/// - `loop_mode`: whether to loop (0: no loop, 1: loop)
148/// - `argb`: transparent color (A,R,G,B)
149/// - `pattern`: animation frame sequence (e.g. 01020304)
150#[derive(Debug, Clone, PartialEq, Eq, Hash)]
151#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
152pub struct SwBgaEvent {
153    /// Frame interval (ms), e.g. 60FPS=17.
154    pub frame_rate: u32,
155    /// Total animation duration (ms), 0 means while key is held.
156    pub total_time: u32,
157    /// Applicable key channel (e.g. 11-19, 21-29).
158    pub line: u8,
159    /// Whether to loop (0: no loop, 1: loop).
160    pub loop_mode: bool,
161    /// Transparent color (A,R,G,B).
162    pub argb: Argb,
163    /// Animation frame sequence (e.g. 01020304).
164    pub pattern: String,
165}
166
167/// BM98 `#ExtChr` extended character customization event.
168///
169/// Used for `#ExtChr` command, implements custom UI element image replacement.
170/// - `sprite_num`: character index to replace `[0-1023]`
171/// - `bmp_num`: BMP index (hex to decimal, or `-1`/`-257`, etc.)
172/// - `start_x`/`start_y`: crop start point
173/// - `end_x`/`end_y`: crop end point
174/// - `offset_x`/`offset_y`: offset (optional)
175/// - `abs_x`/`abs_y`: absolute coordinate (optional)
176#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
177#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
178pub struct ExtChrEvent {
179    /// Character index to replace [0-1023]
180    pub sprite_num: i32,
181    /// BMP index (hex to decimal, or -1/-257, etc.)
182    pub bmp_num: i32,
183    /// Crop start point
184    pub start_x: i32,
185    /// Crop start point
186    pub start_y: i32,
187    /// Crop end point
188    pub end_x: i32,
189    /// Crop end point
190    pub end_y: i32,
191    /// Offset (optional)
192    pub offset_x: Option<i32>,
193    /// Offset (optional)
194    pub offset_y: Option<i32>,
195    /// Absolute coordinate (optional)
196    pub abs_x: Option<i32>,
197    /// Absolute coordinate (optional)
198    pub abs_y: Option<i32>,
199}
200
201/// WAVCMD parameter type.
202///
203/// - Pitch: pitch (0-127, 60 is C6)
204/// - Volume: volume percent (usually 0-100)
205/// - Time: playback time (ms*0.5, 0 means original length)
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
207#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
208pub enum WavCmdParam {
209    /// Pitch (0-127, 60 is C6)
210    Pitch,
211    /// Volume percent (0-100 is recommended. Larger than 100 value is not recommended.)
212    Volume,
213    /// Playback time (ms*0.5, 0 means original length)
214    Time,
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn test_exwav_pan_try_from() {
223        // Valid values
224        assert!(ExWavPan::try_from(0).is_ok());
225        assert!(ExWavPan::try_from(10000).is_ok());
226        assert!(ExWavPan::try_from(-10000).is_ok());
227        assert!(ExWavPan::try_from(5000).is_ok());
228        assert!(ExWavPan::try_from(-5000).is_ok());
229
230        // Invalid values
231        assert!(ExWavPan::try_from(10001).is_err());
232        assert!(ExWavPan::try_from(-10001).is_err());
233        assert!(ExWavPan::try_from(i64::MAX).is_err());
234        assert!(ExWavPan::try_from(i64::MIN).is_err());
235    }
236
237    #[test]
238    fn test_exwav_volume_try_from() {
239        // Valid values
240        assert!(ExWavVolume::try_from(0).is_ok());
241        assert!(ExWavVolume::try_from(-10000).is_ok());
242        assert!(ExWavVolume::try_from(-5000).is_ok());
243
244        // Invalid values
245        assert!(ExWavVolume::try_from(1).is_err());
246        assert!(ExWavVolume::try_from(-10001).is_err());
247        assert!(ExWavVolume::try_from(i64::MAX).is_err());
248        assert!(ExWavVolume::try_from(i64::MIN).is_err());
249    }
250
251    #[test]
252    fn test_exwav_frequency_try_from() {
253        // Valid values
254        assert!(ExWavFrequency::try_from(100).is_ok());
255        assert!(ExWavFrequency::try_from(100000).is_ok());
256        assert!(ExWavFrequency::try_from(50000).is_ok());
257
258        // Invalid values
259        assert!(ExWavFrequency::try_from(99).is_err());
260        assert!(ExWavFrequency::try_from(100001).is_err());
261        assert!(ExWavFrequency::try_from(0).is_err());
262        assert!(ExWavFrequency::try_from(u64::MAX).is_err());
263    }
264
265    #[test]
266    fn test_exwav_values() {
267        // Test value() method
268        let pan = ExWavPan::try_from(5000).unwrap();
269        assert_eq!(pan.value(), 5000);
270
271        let volume = ExWavVolume::try_from(-5000).unwrap();
272        assert_eq!(volume.value(), -5000);
273
274        let frequency = ExWavFrequency::try_from(48000).unwrap();
275        assert_eq!(frequency.value(), 48000);
276    }
277
278    #[test]
279    fn test_exwav_defaults() {
280        assert_eq!(ExWavPan::default().value(), 0);
281        assert_eq!(ExWavVolume::default().value(), 0);
282    }
283}