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}