onedrop_parser/
parser.rs

1//! Parser implementation for .milk files.
2
3use crate::error::{ParseError, Result};
4use crate::preset::*;
5
6/// Parse a complete .milk preset file.
7pub fn parse_milk_preset(input: &str) -> Result<MilkPreset> {
8    let mut preset = MilkPreset::default();
9    let mut lines = input.lines().enumerate();
10
11    // Parse header
12    for (_line_num, line) in lines.by_ref() {
13        let line = line.trim();
14        if line.is_empty() {
15            continue;
16        }
17
18        if line.starts_with("MILKDROP_PRESET_VERSION=") {
19            preset.version = parse_version_line(line)?;
20        } else if line.starts_with("PSVERSION_WARP=") {
21            preset.ps_version_warp = parse_psversion_line(line)?;
22        } else if line.starts_with("PSVERSION_COMP=") {
23            preset.ps_version_comp = parse_psversion_line(line)?;
24        } else if line.starts_with("[preset") {
25            // Found preset section, break to parse body
26            break;
27        }
28    }
29
30    // Parse preset body
31    for (_line_num, line) in lines {
32        let line = line.trim();
33        if line.is_empty() {
34            continue;
35        }
36
37        // Parse per-frame init equations (must check before per_frame_*)
38        if line.starts_with("per_frame_init_") {
39            if let Some(equation) = parse_equation_line(line) {
40                preset.per_frame_init_equations.push(equation);
41            }
42        }
43        // Parse per-frame equations
44        else if line.starts_with("per_frame_") {
45            if let Some(equation) = parse_equation_line(line) {
46                preset.per_frame_equations.push(equation);
47            }
48        }
49        // Parse per-pixel equations
50        else if line.starts_with("per_pixel_") {
51            if let Some(equation) = parse_equation_line(line) {
52                preset.per_pixel_equations.push(equation);
53            }
54        }
55        // Parse warp shader
56        else if line.starts_with("warp_") {
57            let shader_line = parse_shader_line(line);
58            if let Some(ref mut shader) = preset.warp_shader {
59                shader.push_str(&shader_line);
60                shader.push('\n');
61            } else {
62                preset.warp_shader = Some(shader_line + "\n");
63            }
64        }
65        // Parse comp shader
66        else if line.starts_with("comp_") {
67            let shader_line = parse_shader_line(line);
68            if let Some(ref mut shader) = preset.comp_shader {
69                shader.push_str(&shader_line);
70                shader.push('\n');
71            } else {
72                preset.comp_shader = Some(shader_line + "\n");
73            }
74        }
75        // Parse wavecode (scalar params: wavecode_N_*=...)
76        else if line.starts_with("wavecode_") {
77            parse_wavecode_line(line, &mut preset.waves)?;
78        }
79        // Parse shapecode (scalar params: shapecode_N_*=...)
80        else if line.starts_with("shapecode_") {
81            parse_shapecode_line(line, &mut preset.shapes)?;
82        }
83        // Parse custom-wave equations (wave_N_per_frameX, wave_N_per_pointX, wave_N_initX)
84        else if is_indexed_block(line, "wave_") {
85            parse_wave_equation_line(line, &mut preset.waves)?;
86        }
87        // Parse custom-shape equations (shape_N_per_frameX, shape_N_initX)
88        else if is_indexed_block(line, "shape_") {
89            parse_shape_equation_line(line, &mut preset.shapes)?;
90        }
91        // Parse regular parameters
92        else if let Some((key, value)) = line.split_once('=') {
93            parse_parameter(key.trim(), value.trim(), &mut preset.parameters)?;
94        }
95    }
96
97    Ok(preset)
98}
99
100/// Parse version line (e.g., "MILKDROP_PRESET_VERSION=201")
101fn parse_version_line(line: &str) -> Result<u32> {
102    line.split('=')
103        .nth(1)
104        .and_then(|v| v.trim().parse().ok())
105        .ok_or_else(|| ParseError::InvalidVersion(line.to_string()))
106}
107
108/// Parse PS version line
109fn parse_psversion_line(line: &str) -> Result<u32> {
110    line.split('=')
111        .nth(1)
112        .and_then(|v| v.trim().parse().ok())
113        .ok_or_else(|| ParseError::ParseFailed(format!("Invalid PSVERSION: {}", line)))
114}
115
116/// Parse equation line (e.g., "per_frame_1=wave_r = 0.5;")
117fn parse_equation_line(line: &str) -> Option<String> {
118    line.split_once('=')
119        .map(|(_, equation)| equation.trim().to_string())
120}
121
122/// Parse shader line (e.g., "warp_1=`shader_body")
123fn parse_shader_line(line: &str) -> String {
124    line.split_once('=')
125        .map(|(_, code)| {
126            // Remove backtick prefix if present
127            code.trim().trim_start_matches('`').to_string()
128        })
129        .unwrap_or_default()
130}
131
132/// Parse a parameter and store it in PresetParameters
133fn parse_parameter(key: &str, value: &str, params: &mut PresetParameters) -> Result<()> {
134    // Helper to parse float
135    let parse_f32 = |v: &str| -> Result<f32> {
136        v.parse().map_err(|_| ParseError::InvalidParameter {
137            name: key.to_string(),
138            value: v.to_string(),
139            reason: "Expected float".to_string(),
140        })
141    };
142
143    // Helper to parse int
144    let parse_i32 = |v: &str| -> Result<i32> {
145        v.parse().map_err(|_| ParseError::InvalidParameter {
146            name: key.to_string(),
147            value: v.to_string(),
148            reason: "Expected integer".to_string(),
149        })
150    };
151
152    // Helper to parse bool
153    let parse_bool = |v: &str| -> Result<bool> {
154        match v {
155            "0" => Ok(false),
156            "1" => Ok(true),
157            _ => Err(ParseError::InvalidParameter {
158                name: key.to_string(),
159                value: v.to_string(),
160                reason: "Expected 0 or 1".to_string(),
161            }),
162        }
163    };
164
165    match key {
166        // Float parameters
167        "fRating" => params.f_rating = parse_f32(value)?,
168        "fGammaAdj" => params.f_gamma_adj = parse_f32(value)?,
169        "fDecay" => params.f_decay = parse_f32(value)?,
170        "fVideoEchoZoom" => params.f_video_echo_zoom = parse_f32(value)?,
171        "fVideoEchoAlpha" => params.f_video_echo_alpha = parse_f32(value)?,
172        "fWaveAlpha" => params.f_wave_alpha = parse_f32(value)?,
173        "fWaveScale" => params.f_wave_scale = parse_f32(value)?,
174        "fWaveSmoothing" => params.f_wave_smoothing = parse_f32(value)?,
175        "fWaveParam" => params.f_wave_param = parse_f32(value)?,
176        "fModWaveAlphaStart" => params.f_mod_wave_alpha_start = parse_f32(value)?,
177        "fModWaveAlphaEnd" => params.f_mod_wave_alpha_end = parse_f32(value)?,
178        "fWarpAnimSpeed" => params.f_warp_anim_speed = parse_f32(value)?,
179        "fWarpScale" => params.f_warp_scale = parse_f32(value)?,
180        "fZoomExponent" => params.f_zoom_exponent = parse_f32(value)?,
181        "fShader" => params.f_shader = parse_f32(value)?,
182
183        // Motion parameters
184        "zoom" => params.zoom = parse_f32(value)?,
185        "rot" => params.rot = parse_f32(value)?,
186        "cx" => params.cx = parse_f32(value)?,
187        "cy" => params.cy = parse_f32(value)?,
188        "dx" => params.dx = parse_f32(value)?,
189        "dy" => params.dy = parse_f32(value)?,
190        "warp" => params.warp = parse_f32(value)?,
191        "sx" => params.sx = parse_f32(value)?,
192        "sy" => params.sy = parse_f32(value)?,
193
194        // Wave colors
195        "wave_r" => params.wave_r = parse_f32(value)?,
196        "wave_g" => params.wave_g = parse_f32(value)?,
197        "wave_b" => params.wave_b = parse_f32(value)?,
198        "wave_x" => params.wave_x = parse_f32(value)?,
199        "wave_y" => params.wave_y = parse_f32(value)?,
200
201        // Borders
202        "ob_size" => params.ob_size = parse_f32(value)?,
203        "ob_r" => params.ob_r = parse_f32(value)?,
204        "ob_g" => params.ob_g = parse_f32(value)?,
205        "ob_b" => params.ob_b = parse_f32(value)?,
206        "ob_a" => params.ob_a = parse_f32(value)?,
207        "ib_size" => params.ib_size = parse_f32(value)?,
208        "ib_r" => params.ib_r = parse_f32(value)?,
209        "ib_g" => params.ib_g = parse_f32(value)?,
210        "ib_b" => params.ib_b = parse_f32(value)?,
211        "ib_a" => params.ib_a = parse_f32(value)?,
212
213        // Motion vectors
214        "nMotionVectorsX" => params.n_motion_vectors_x = parse_f32(value)?,
215        "nMotionVectorsY" => params.n_motion_vectors_y = parse_f32(value)?,
216        "mv_dx" => params.mv_dx = parse_f32(value)?,
217        "mv_dy" => params.mv_dy = parse_f32(value)?,
218        "mv_l" => params.mv_l = parse_f32(value)?,
219        "mv_r" => params.mv_r = parse_f32(value)?,
220        "mv_g" => params.mv_g = parse_f32(value)?,
221        "mv_b" => params.mv_b = parse_f32(value)?,
222        "mv_a" => params.mv_a = parse_f32(value)?,
223
224        // Beat detection
225        "b1n" => params.b1n = parse_f32(value)?,
226        "b2n" => params.b2n = parse_f32(value)?,
227        "b3n" => params.b3n = parse_f32(value)?,
228        "b1x" => params.b1x = parse_f32(value)?,
229        "b2x" => params.b2x = parse_f32(value)?,
230        "b3x" => params.b3x = parse_f32(value)?,
231        "b1ed" => params.b1ed = parse_f32(value)?,
232
233        // Integer parameters
234        "nVideoEchoOrientation" => params.n_video_echo_orientation = parse_i32(value)?,
235        "nWaveMode" => params.n_wave_mode = parse_i32(value)?,
236
237        // Boolean parameters
238        "bAdditiveWaves" => params.b_additive_waves = parse_bool(value)?,
239        "bWaveDots" => params.b_wave_dots = parse_bool(value)?,
240        "bWaveThick" => params.b_wave_thick = parse_bool(value)?,
241        "bModWaveAlphaByVolume" => params.b_mod_wave_alpha_by_volume = parse_bool(value)?,
242        "bMaximizeWaveColor" => params.b_maximize_wave_color = parse_bool(value)?,
243        "bTexWrap" => params.b_tex_wrap = parse_bool(value)?,
244        "bDarkenCenter" => params.b_darken_center = parse_bool(value)?,
245        "bRedBlueStereo" => params.b_red_blue_stereo = parse_bool(value)?,
246        "bBrighten" => params.b_brighten = parse_bool(value)?,
247        "bDarken" => params.b_darken = parse_bool(value)?,
248        "bSolarize" => params.b_solarize = parse_bool(value)?,
249        "bInvert" => params.b_invert = parse_bool(value)?,
250
251        // Unknown parameters go to extra map
252        _ => {
253            params.extra.insert(key.to_string(), value.to_string());
254        }
255    }
256
257    Ok(())
258}
259
260/// Build a default `WaveCode` for index N. Used when wavecode_N_* or wave_N_* lines
261/// reference a wave that hasn't been seen yet — both code paths must agree on defaults.
262fn default_wave_code(index: usize) -> WaveCode {
263    WaveCode {
264        index,
265        enabled: false,
266        samples: 512,
267        sep: 0,
268        b_spectrum: false,
269        b_use_dots: false,
270        b_draw_thick: false,
271        b_additive: false,
272        scaling: 1.0,
273        smoothing: 0.5,
274        r: 1.0,
275        g: 1.0,
276        b: 1.0,
277        a: 1.0,
278        per_frame_equations: Vec::new(),
279        per_point_equations: Vec::new(),
280        per_frame_init_equations: Vec::new(),
281    }
282}
283
284/// Build a default `ShapeCode` for index N. Mirrors `default_wave_code`.
285fn default_shape_code(index: usize) -> ShapeCode {
286    ShapeCode {
287        index,
288        enabled: false,
289        sides: 4,
290        additive: false,
291        thick_outline: false,
292        textured: false,
293        num_inst: 1,
294        x: 0.5,
295        y: 0.5,
296        rad: 0.1,
297        ang: 0.0,
298        tex_ang: 0.0,
299        tex_zoom: 1.0,
300        r: 1.0,
301        g: 1.0,
302        b: 1.0,
303        a: 1.0,
304        r2: 0.0,
305        g2: 0.0,
306        b2: 0.0,
307        a2: 0.0,
308        border_r: 1.0,
309        border_g: 1.0,
310        border_b: 1.0,
311        border_a: 0.0,
312        per_frame_equations: Vec::new(),
313        per_frame_init_equations: Vec::new(),
314    }
315}
316
317/// Returns true if `line` starts with `<prefix><digit>_` — i.e., an indexed block like
318/// `wave_0_per_frame1=...` or `shape_3_init1=...`. Used to distinguish from scalar
319/// param names like `wave_r=...` that share the `wave_` prefix.
320fn is_indexed_block(line: &str, prefix: &str) -> bool {
321    let Some(rest) = line.strip_prefix(prefix) else {
322        return false;
323    };
324    let bytes = rest.as_bytes();
325    let mut i = 0;
326    while i < bytes.len() && bytes[i].is_ascii_digit() {
327        i += 1;
328    }
329    i > 0 && i < bytes.len() && bytes[i] == b'_'
330}
331
332/// Parse a custom-wave equation line:
333///   `wave_N_per_frameM=expr`  → per_frame_equations
334///   `wave_N_per_pointM=expr`  → per_point_equations
335///   `wave_N_initM=expr`       → per_frame_init_equations
336fn parse_wave_equation_line(line: &str, waves: &mut Vec<WaveCode>) -> Result<()> {
337    let Some((key, value)) = line.split_once('=') else {
338        return Ok(());
339    };
340    let rest = key.strip_prefix("wave_").unwrap_or("");
341    let Some((index_str, kind)) = rest.split_once('_') else {
342        return Ok(());
343    };
344    let Ok(index) = index_str.parse::<usize>() else {
345        return Ok(());
346    };
347    while waves.len() <= index {
348        waves.push(default_wave_code(waves.len()));
349    }
350    let wave = &mut waves[index];
351    let equation = value.trim().to_string();
352    if equation.is_empty() {
353        return Ok(());
354    }
355    if kind.starts_with("per_frame") {
356        wave.per_frame_equations.push(equation);
357    } else if kind.starts_with("per_point") {
358        wave.per_point_equations.push(equation);
359    } else if kind.starts_with("init") {
360        wave.per_frame_init_equations.push(equation);
361    }
362    Ok(())
363}
364
365/// Parse a custom-shape equation line:
366///   `shape_N_per_frameM=expr` → per_frame_equations
367///   `shape_N_initM=expr`      → per_frame_init_equations
368fn parse_shape_equation_line(line: &str, shapes: &mut Vec<ShapeCode>) -> Result<()> {
369    let Some((key, value)) = line.split_once('=') else {
370        return Ok(());
371    };
372    let rest = key.strip_prefix("shape_").unwrap_or("");
373    let Some((index_str, kind)) = rest.split_once('_') else {
374        return Ok(());
375    };
376    let Ok(index) = index_str.parse::<usize>() else {
377        return Ok(());
378    };
379    while shapes.len() <= index {
380        shapes.push(default_shape_code(shapes.len()));
381    }
382    let shape = &mut shapes[index];
383    let equation = value.trim().to_string();
384    if equation.is_empty() {
385        return Ok(());
386    }
387    if kind.starts_with("per_frame") {
388        shape.per_frame_equations.push(equation);
389    } else if kind.starts_with("init") {
390        shape.per_frame_init_equations.push(equation);
391    }
392    Ok(())
393}
394
395/// Parse wavecode line
396fn parse_wavecode_line(line: &str, waves: &mut Vec<WaveCode>) -> Result<()> {
397    // Extract wave index and parameter name
398    // Format: wavecode_N_param=value
399    let parts: Vec<&str> = line.split('_').collect();
400    if parts.len() < 3 {
401        return Ok(()); // Skip malformed lines
402    }
403
404    let index: usize = parts[1].parse().unwrap_or(0);
405    let param_and_value = line.split_once('=');
406
407    if let Some((param_full, value)) = param_and_value {
408        let param = param_full.split('_').skip(2).collect::<Vec<_>>().join("_");
409
410        // Ensure wave exists
411        while waves.len() <= index {
412            waves.push(default_wave_code(waves.len()));
413        }
414
415        // Parse parameter
416        let wave = &mut waves[index];
417        match param.as_str() {
418            "enabled" => wave.enabled = value == "1",
419            "samples" => wave.samples = value.parse().unwrap_or(512),
420            "sep" => wave.sep = value.parse().unwrap_or(0),
421            "bSpectrum" => wave.b_spectrum = value == "1",
422            "bUseDots" => wave.b_use_dots = value == "1",
423            "bDrawThick" => wave.b_draw_thick = value == "1",
424            "bAdditive" => wave.b_additive = value == "1",
425            "scaling" => wave.scaling = value.parse().unwrap_or(1.0),
426            "smoothing" => wave.smoothing = value.parse().unwrap_or(0.5),
427            "r" => wave.r = value.parse().unwrap_or(1.0),
428            "g" => wave.g = value.parse().unwrap_or(1.0),
429            "b" => wave.b = value.parse().unwrap_or(1.0),
430            "a" => wave.a = value.parse().unwrap_or(1.0),
431            _ => {} // Ignore unknown parameters
432        }
433    }
434
435    Ok(())
436}
437
438/// Parse shapecode line
439fn parse_shapecode_line(line: &str, shapes: &mut Vec<ShapeCode>) -> Result<()> {
440    // Extract shape index and parameter name
441    // Format: shapecode_N_param=value
442    let parts: Vec<&str> = line.split('_').collect();
443    if parts.len() < 3 {
444        return Ok(()); // Skip malformed lines
445    }
446
447    let index: usize = parts[1].parse().unwrap_or(0);
448    let param_and_value = line.split_once('=');
449
450    if let Some((param_full, value)) = param_and_value {
451        let param = param_full.split('_').skip(2).collect::<Vec<_>>().join("_");
452
453        // Ensure shape exists
454        while shapes.len() <= index {
455            shapes.push(default_shape_code(shapes.len()));
456        }
457
458        // Parse parameter
459        let shape = &mut shapes[index];
460        match param.as_str() {
461            "enabled" => shape.enabled = value == "1",
462            "sides" => shape.sides = value.parse().unwrap_or(4),
463            "additive" => shape.additive = value == "1",
464            "thickOutline" => shape.thick_outline = value == "1",
465            "textured" => shape.textured = value == "1",
466            "num_inst" | "num inst" => shape.num_inst = value.parse().unwrap_or(1),
467            "x" => shape.x = value.parse().unwrap_or(0.5),
468            "y" => shape.y = value.parse().unwrap_or(0.5),
469            "rad" => shape.rad = value.parse().unwrap_or(0.1),
470            "ang" => shape.ang = value.parse().unwrap_or(0.0),
471            "tex_ang" | "tex ang" => shape.tex_ang = value.parse().unwrap_or(0.0),
472            "tex_zoom" | "tex zoom" => shape.tex_zoom = value.parse().unwrap_or(1.0),
473            "r" => shape.r = value.parse().unwrap_or(1.0),
474            "g" => shape.g = value.parse().unwrap_or(1.0),
475            "b" => shape.b = value.parse().unwrap_or(1.0),
476            "a" => shape.a = value.parse().unwrap_or(1.0),
477            "r2" => shape.r2 = value.parse().unwrap_or(0.0),
478            "g2" => shape.g2 = value.parse().unwrap_or(0.0),
479            "b2" => shape.b2 = value.parse().unwrap_or(0.0),
480            "a2" => shape.a2 = value.parse().unwrap_or(0.0),
481            "border_r" | "border r" => shape.border_r = value.parse().unwrap_or(1.0),
482            "border_g" | "border g" => shape.border_g = value.parse().unwrap_or(1.0),
483            "border_b" | "border b" => shape.border_b = value.parse().unwrap_or(1.0),
484            "border_a" | "border a" => shape.border_a = value.parse().unwrap_or(0.0),
485            _ => {} // Ignore unknown parameters
486        }
487    }
488
489    Ok(())
490}
491
492#[cfg(test)]
493mod tests {
494    use super::*;
495
496    #[test]
497    fn test_parse_version() {
498        let line = "MILKDROP_PRESET_VERSION=201";
499        assert_eq!(parse_version_line(line).unwrap(), 201);
500    }
501
502    #[test]
503    fn test_parse_equation() {
504        let line = "per_frame_1=wave_r = 0.5;";
505        assert_eq!(parse_equation_line(line), Some("wave_r = 0.5;".to_string()));
506    }
507
508    #[test]
509    fn test_parse_shader() {
510        let line = "warp_1=`shader_body";
511        assert_eq!(parse_shader_line(line), "shader_body");
512    }
513
514    #[test]
515    fn test_is_indexed_block() {
516        assert!(is_indexed_block("wave_0_per_frame1=...", "wave_"));
517        assert!(is_indexed_block("wave_3_init1=...", "wave_"));
518        assert!(is_indexed_block("shape_2_per_frame4=...", "shape_"));
519        // Single-token scalars must NOT be treated as indexed blocks
520        assert!(!is_indexed_block("wave_r=1.0", "wave_"));
521        assert!(!is_indexed_block("wave_g=0.5", "wave_"));
522        assert!(!is_indexed_block("wave_x=0.5", "wave_"));
523        assert!(!is_indexed_block("warp_1=`code`", "wave_"));
524    }
525
526    #[test]
527    fn test_parse_wave_equation_line() {
528        let mut waves: Vec<WaveCode> = Vec::new();
529        parse_wave_equation_line("wave_0_init1=t1 = 0.5;", &mut waves).unwrap();
530        parse_wave_equation_line("wave_0_per_frame1=t1 = t1 + 0.1;", &mut waves).unwrap();
531        parse_wave_equation_line("wave_0_per_point1=x = sample; y = value1;", &mut waves).unwrap();
532        parse_wave_equation_line("wave_0_per_point2=r = 1.0; g = 0.5;", &mut waves).unwrap();
533        assert_eq!(waves.len(), 1);
534        assert_eq!(waves[0].per_frame_init_equations, vec!["t1 = 0.5;"]);
535        assert_eq!(waves[0].per_frame_equations, vec!["t1 = t1 + 0.1;"]);
536        assert_eq!(
537            waves[0].per_point_equations,
538            vec!["x = sample; y = value1;", "r = 1.0; g = 0.5;"]
539        );
540    }
541
542    #[test]
543    fn test_parse_wave_equation_extends_vec_to_index() {
544        let mut waves: Vec<WaveCode> = Vec::new();
545        parse_wave_equation_line("wave_3_init1=t1 = 0.5;", &mut waves).unwrap();
546        assert_eq!(waves.len(), 4);
547        assert_eq!(waves[3].index, 3);
548        assert_eq!(waves[0].per_frame_init_equations.len(), 0);
549    }
550
551    #[test]
552    fn test_parse_shape_equation_line() {
553        let mut shapes: Vec<ShapeCode> = Vec::new();
554        parse_shape_equation_line("shape_0_init1=q1 = 0;", &mut shapes).unwrap();
555        parse_shape_equation_line("shape_0_per_frame1=x = 0.5;", &mut shapes).unwrap();
556        assert_eq!(shapes.len(), 1);
557        assert_eq!(shapes[0].per_frame_init_equations, vec!["q1 = 0;"]);
558        assert_eq!(shapes[0].per_frame_equations, vec!["x = 0.5;"]);
559    }
560
561    #[test]
562    fn test_per_frame_init_routed_to_init_vec() {
563        let src = "MILKDROP_PRESET_VERSION=201\n[preset00]\n\
564                   per_frame_init_1=t1 = 0;\n\
565                   per_frame_1=t1 = t1 + 1;\n";
566        let preset = parse_milk_preset(src).unwrap();
567        assert_eq!(preset.per_frame_init_equations, vec!["t1 = 0;"]);
568        assert_eq!(preset.per_frame_equations, vec!["t1 = t1 + 1;"]);
569    }
570
571    #[test]
572    fn test_full_preset_with_custom_wave() {
573        let src = "MILKDROP_PRESET_VERSION=201\n[preset00]\n\
574                   wavecode_0_enabled=1\n\
575                   wavecode_0_samples=64\n\
576                   wavecode_0_bSpectrum=1\n\
577                   wave_0_init1=q1 = 0;\n\
578                   wave_0_per_frame1=q1 = q1 + 0.01;\n\
579                   wave_0_per_point1=x = sample; y = value1 * 0.5;\n";
580        let preset = parse_milk_preset(src).unwrap();
581        assert_eq!(preset.waves.len(), 1);
582        let w = &preset.waves[0];
583        assert!(w.enabled);
584        assert_eq!(w.samples, 64);
585        assert!(w.b_spectrum);
586        assert_eq!(w.per_frame_init_equations, vec!["q1 = 0;"]);
587        assert_eq!(w.per_frame_equations, vec!["q1 = q1 + 0.01;"]);
588        assert_eq!(w.per_point_equations, vec!["x = sample; y = value1 * 0.5;"]);
589    }
590
591    #[test]
592    fn test_full_preset_with_custom_shape() {
593        let src = "MILKDROP_PRESET_VERSION=201\n[preset00]\n\
594                   shapecode_0_enabled=1\n\
595                   shapecode_0_sides=6\n\
596                   shapecode_0_num_inst=8\n\
597                   shapecode_0_textured=1\n\
598                   shapecode_0_thickOutline=1\n\
599                   shapecode_0_additive=1\n\
600                   shapecode_0_x=0.4\n\
601                   shapecode_0_y=0.6\n\
602                   shapecode_0_rad=0.15\n\
603                   shapecode_0_r=0.8\n\
604                   shapecode_0_g=0.2\n\
605                   shapecode_0_b=0.5\n\
606                   shapecode_0_a=0.9\n\
607                   shapecode_0_r2=0.0\n\
608                   shapecode_0_g2=0.6\n\
609                   shapecode_0_b2=1.0\n\
610                   shapecode_0_a2=0.4\n\
611                   shapecode_0_border_r=1.0\n\
612                   shapecode_0_border_g=1.0\n\
613                   shapecode_0_border_b=1.0\n\
614                   shapecode_0_border_a=0.5\n\
615                   shape_0_init1=t1 = 0;\n\
616                   shape_0_init2=t2 = 42;\n\
617                   shape_0_per_frame1=ang = ang + 0.05 * instance;\n\
618                   shape_0_per_frame2=rad = rad + 0.001;\n";
619        let preset = parse_milk_preset(src).unwrap();
620        assert_eq!(preset.shapes.len(), 1);
621        let s = &preset.shapes[0];
622        assert!(s.enabled);
623        assert_eq!(s.sides, 6);
624        assert_eq!(s.num_inst, 8);
625        assert!(s.textured);
626        assert!(s.thick_outline);
627        assert!(s.additive);
628        assert!((s.x - 0.4).abs() < 1e-5);
629        assert!((s.y - 0.6).abs() < 1e-5);
630        assert!((s.r2 - 0.0).abs() < 1e-5);
631        assert!((s.g2 - 0.6).abs() < 1e-5);
632        assert!((s.border_a - 0.5).abs() < 1e-5);
633        assert_eq!(s.per_frame_init_equations, vec!["t1 = 0;", "t2 = 42;"]);
634        assert_eq!(
635            s.per_frame_equations,
636            vec!["ang = ang + 0.05 * instance;", "rad = rad + 0.001;"]
637        );
638    }
639}