Skip to main content

rgc_chart/models/osu/
events.rs

1use std::str::FromStr;
2
3#[derive(Debug, Clone, PartialEq)]
4pub struct Event {
5    pub event_type: String,
6    
7    pub start_time: i32,
8    
9    pub event_params: Vec<String>,
10}
11
12#[derive(Debug, Clone, PartialEq)]
13pub struct Background {
14    pub filename: String,
15    
16    pub x_offset: i32,
17    
18    pub y_offset: i32,
19}
20
21#[derive(Debug, Clone, PartialEq)]
22pub struct Video {
23    pub start_time: i32,
24    
25    pub filename: String,
26    
27    pub x_offset: i32,
28    
29    pub y_offset: i32,
30}
31
32#[derive(Debug, Clone, PartialEq)]
33pub struct Break {
34    pub start_time: i32,
35    
36    pub end_time: i32,
37}
38
39#[derive(Debug, Clone, PartialEq)]
40pub struct Sample {
41    pub start_time: i32,
42    
43    pub layer: i32,
44    
45    pub filename: String,
46    
47    pub volume: u8,
48}
49
50#[derive(Debug, Clone, PartialEq)]
51pub struct Events {
52    pub background: Option<Background>,
53    
54    pub video: Option<Video>,
55    
56    pub breaks: Vec<Break>,
57    
58    pub samples: Vec<Sample>,
59    
60    pub raw_events: String,
61}
62
63impl Default for Events {
64    fn default() -> Self {
65        Self {
66            background: None,
67            video: None,
68            breaks: Vec::new(),
69            samples: Vec::new(),
70            raw_events: String::new(),
71        }
72    }
73}
74
75impl FromStr for Event {
76    type Err = String;
77    
78    fn from_str(s: &str) -> Result<Self, Self::Err> {
79        let parts: Vec<&str> = s.split(',').collect();
80        
81        if parts.len() < 2 {
82            return Err(format!("Expected at least 2 comma-separated values, found {}", parts.len()));
83        }
84        
85        let event_type = parts[0].trim().to_string();
86        
87        let start_time = parts[1].parse::<i32>()
88            .map_err(|_| format!("Invalid start time value: {}", parts[1]))?;
89        
90        let event_params = parts[2..].iter()
91            .map(|param| param.trim().to_string())
92            .collect();
93        
94        Ok(Event {
95            event_type,
96            start_time,
97            event_params,
98        })
99    }
100}
101
102impl FromStr for Events {
103    type Err = String;
104    
105    fn from_str(s: &str) -> Result<Self, Self::Err> {
106        let mut events_section = Events::default();
107        events_section.raw_events = s.to_string();
108        
109        for line in s.lines() {
110            let line = line.trim();
111            if line.is_empty() || line.starts_with("//") {
112                continue;
113            }
114            
115            let event = Event::from_str(line)?;
116            
117            match event.event_type.as_str() {
118                "0" => {
119                    if event.event_params.len() >= 1 {
120                        let filename = event.event_params[0]
121                            .trim_matches('"')
122                            .to_string();
123                        
124                        let x_offset = if event.event_params.len() >= 2 {
125                            event.event_params[1].parse::<i32>().unwrap_or(0)
126                        } else {
127                            0
128                        };
129                        
130                        let y_offset = if event.event_params.len() >= 3 {
131                            event.event_params[2].parse::<i32>().unwrap_or(0)
132                        } else {
133                            0
134                        };
135                        
136                        events_section.background = Some(Background {
137                            filename,
138                            x_offset,
139                            y_offset,
140                        });
141                    }
142                }
143                "1" | "Video" => {
144                    if event.event_params.len() >= 1 {
145                        let filename = event.event_params[0]
146                            .trim_matches('"')
147                            .to_string();
148                        
149                        let x_offset = if event.event_params.len() >= 2 {
150                            event.event_params[1].parse::<i32>().unwrap_or(0)
151                        } else {
152                            0
153                        };
154                        
155                        let y_offset = if event.event_params.len() >= 3 {
156                            event.event_params[2].parse::<i32>().unwrap_or(0)
157                        } else {
158                            0
159                        };
160                        
161                        events_section.video = Some(Video {
162                            start_time: event.start_time,
163                            filename,
164                            x_offset,
165                            y_offset,
166                        });
167                    }
168                }
169                "2" | "Break" => {
170                    if event.event_params.len() >= 1 {
171                        let end_time = event.event_params[0].parse::<i32>()
172                            .map_err(|_| format!("Invalid break end time: {}", event.event_params[0]))?;
173                        
174                        events_section.breaks.push(Break {
175                            start_time: event.start_time,
176                            end_time,
177                        });
178                    }
179                }
180                "Sample" => {
181                    if event.event_params.len() >= 2 {
182                        let layer = event.event_params[0].parse::<i32>()
183                            .map_err(|_| format!("Invalid sample layer: {}", event.event_params[0]))?;
184                        
185                        let filename = event.event_params[1]
186                            .trim_matches('"')
187                            .to_string();
188                        
189                        let volume = if event.event_params.len() >= 3 {
190                            event.event_params[2].parse::<u8>().unwrap_or(100)
191                        } else {
192                            100
193                        };
194                        
195                        events_section.samples.push(Sample {
196                            start_time: event.start_time,
197                            layer,
198                            filename,
199                            volume,
200                        });
201                    }
202                }
203                _ => { }
204            }
205        }
206        
207        Ok(events_section)
208    }
209}
210
211impl Event {
212    pub fn new(event_type: String, start_time: i32, event_params: Vec<String>) -> Self {
213        Self {
214            event_type,
215            start_time,
216            event_params,
217        }
218    }
219    
220    pub fn is_background(&self) -> bool {
221        self.event_type == "0"
222    }
223    
224    pub fn is_video(&self) -> bool {
225        self.event_type == "1" || self.event_type == "Video"
226    }
227    
228    pub fn is_break(&self) -> bool {
229        self.event_type == "2" || self.event_type == "Break"
230    }
231    
232    pub fn is_sample(&self) -> bool {
233        self.event_type == "Sample"
234    }
235    
236    pub fn to_osu_format(&self) -> String {
237        let mut parts = vec![self.event_type.clone(), self.start_time.to_string()];
238        parts.extend(self.event_params.iter().cloned());
239        parts.join(",")
240    }
241}
242
243impl Background {
244    pub fn new(filename: String, x_offset: i32, y_offset: i32) -> Self {
245        Self {
246            filename,
247            x_offset,
248            y_offset,
249        }
250    }
251    
252    pub fn to_osu_format(&self) -> String {
253        if self.x_offset == 0 && self.y_offset == 0 {
254            format!("0,0,\"{}\"", self.filename)
255        } else {
256            format!("0,0,\"{}\",{},{}", self.filename, self.x_offset, self.y_offset)
257        }
258    }
259}
260
261impl Video {
262    pub fn new(start_time: i32, filename: String, x_offset: i32, y_offset: i32) -> Self {
263        Self {
264            start_time,
265            filename,
266            x_offset,
267            y_offset,
268        }
269    }
270    
271    pub fn to_osu_format(&self) -> String {
272        if self.x_offset == 0 && self.y_offset == 0 {
273            format!("Video,{},\"{}\"", self.start_time, self.filename)
274        } else {
275            format!("Video,{},\"{}\",{},{}", self.start_time, self.filename, self.x_offset, self.y_offset)
276        }
277    }
278}
279
280impl Break {
281    pub fn new(start_time: i32, end_time: i32) -> Self {
282        Self {
283            start_time,
284            end_time,
285        }
286    }
287    
288    pub fn duration(&self) -> i32 {
289        self.end_time - self.start_time
290    }
291    
292    pub fn to_osu_format(&self) -> String {
293        format!("2,{},{}", self.start_time, self.end_time)
294    }
295}
296
297impl Sample {
298    pub fn new(start_time: i32, layer: i32, filename: String, volume: u8) -> Self {
299        Self {
300            start_time,
301            layer,
302            filename,
303            volume,
304        }
305    }
306    
307    pub fn layer_name(&self) -> &'static str {
308        match self.layer {
309            0 => "Background",
310            1 => "Fail",
311            2 => "Pass",
312            3 => "Foreground",
313            _ => "Unknown",
314        }
315    }
316    
317    pub fn to_osu_format(&self) -> String {
318        if self.volume == 100 {
319            format!("Sample,{},{},\"{}\"", self.start_time, self.layer, self.filename)
320        } else {
321            format!("Sample,{},{},\"{}\",{}", self.start_time, self.layer, self.filename, self.volume)
322        }
323    }
324}
325
326impl Events {
327    pub fn new() -> Self {
328        Self::default()
329    }
330    
331    pub fn set_background(&mut self, filename: String, x_offset: i32, y_offset: i32) {
332        self.background = Some(Background::new(filename, x_offset, y_offset));
333    }
334    
335    pub fn set_video(&mut self, start_time: i32, filename: String, x_offset: i32, y_offset: i32) {
336        self.video = Some(Video::new(start_time, filename, x_offset, y_offset));
337    }
338    
339    pub fn add_break(&mut self, start_time: i32, end_time: i32) {
340        self.breaks.push(Break::new(start_time, end_time));
341    }
342    
343    pub fn add_sample(&mut self, start_time: i32, layer: i32, filename: String, volume: u8) {
344        self.samples.push(Sample::new(start_time, layer, filename, volume));
345    }
346    
347    pub fn total_break_time(&self) -> i32 {
348        self.breaks.iter().map(|b| b.duration()).sum()
349    }
350    
351    pub fn has_breaks(&self) -> bool {
352        !self.breaks.is_empty()
353    }
354    
355    pub fn has_background(&self) -> bool {
356        self.background.is_some()
357    }
358    
359    pub fn has_video(&self) -> bool {
360        self.video.is_some()
361    }
362    
363    pub fn has_samples(&self) -> bool {
364        !self.samples.is_empty()
365    }
366    
367    pub fn samples_by_layer(&self, layer: i32) -> Vec<&Sample> {
368        self.samples.iter().filter(|s| s.layer == layer).collect()
369    }
370    
371    pub fn to_osu_format(&self) -> String {
372        let mut result = String::new();
373        
374        result.push_str("//Background and Video events\n");
375        
376        if let Some(ref bg) = self.background {
377            result.push_str(&bg.to_osu_format());
378            result.push('\n');
379        }
380        
381        if let Some(ref video) = self.video {
382            result.push_str(&video.to_osu_format());
383            result.push('\n');
384        }
385        
386        result.push_str("//Break Periods\n");
387        for break_event in &self.breaks {
388            result.push_str(&break_event.to_osu_format());
389            result.push('\n');
390        }
391        
392        result.push_str("//Storyboard Layer 0 (Background)\n");
393        for sample in self.samples_by_layer(0) {
394            result.push_str(&sample.to_osu_format());
395            result.push('\n');
396        }
397        
398        result.push_str("//Storyboard Layer 1 (Fail)\n");
399        for sample in self.samples_by_layer(1) {
400            result.push_str(&sample.to_osu_format());
401            result.push('\n');
402        }
403        
404        result.push_str("//Storyboard Layer 2 (Pass)\n");
405        for sample in self.samples_by_layer(2) {
406            result.push_str(&sample.to_osu_format());
407            result.push('\n');
408        }
409        
410        result.push_str("//Storyboard Layer 3 (Foreground)\n");
411        for sample in self.samples_by_layer(3) {
412            result.push_str(&sample.to_osu_format());
413            result.push('\n');
414        }
415        
416        result.push_str("//Storyboard Layer 4 (Overlay)\n");
417        for sample in &self.samples {
418            if sample.layer >= 4 {
419                result.push_str(&sample.to_osu_format());
420                result.push('\n');
421            }
422        }
423        
424        result.push_str("//Storyboard Sound Samples\n");
425        
426        result
427    }
428}