Skip to main content

supreme_music/
lib.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashSet;
3use std::fs;
4use std::io::Write;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct MusicConfig {
9    pub filename: String,
10    pub halfpipe: bool,
11    pub tracks: bool,
12    pub author: String,
13    pub track: String,
14    pub volume: f32,
15}
16
17#[must_use]
18pub fn discover_music_files(music_dir: &Path) -> Vec<PathBuf> {
19    let mut music_files = Vec::new();
20
21    if let Ok(entries) = fs::read_dir(music_dir) {
22        for entry in entries.flatten() {
23            let path = entry.path();
24            if path.is_file()
25                && path.extension().and_then(|ext| ext.to_str()) == Some("adp")
26                && let Some(file_name) = path.file_name().and_then(|n| n.to_str())
27                && file_name != "menu.adp"
28            {
29                music_files.push(path);
30            }
31        }
32    }
33
34    music_files.sort();
35    music_files
36}
37
38#[must_use]
39pub fn load_music_config(music_dir: &Path) -> Vec<MusicConfig> {
40    let music_config_path = music_dir.join("music_config.txt");
41    let mut configs = Vec::new();
42
43    if let Ok(content) = fs::read_to_string(music_config_path) {
44        // Parse the music config file
45        let mut current_config = MusicConfig {
46            filename: String::new(),
47            halfpipe: false,
48            tracks: false,
49            author: String::new(),
50            track: String::new(),
51            volume: 0.3,
52        };
53
54        let mut in_music_block = false;
55
56        for line in content.lines() {
57            let line = line.trim();
58
59            if line == "Music {" {
60                in_music_block = true;
61                current_config = MusicConfig {
62                    filename: String::new(),
63                    halfpipe: false,
64                    tracks: false,
65                    author: String::new(),
66                    track: String::new(),
67                    volume: 0.3,
68                };
69            } else if line == "};" && in_music_block {
70                in_music_block = false;
71                if !current_config.filename.is_empty() {
72                    configs.push(current_config.clone());
73                }
74                // Reset for next block
75                current_config = MusicConfig {
76                    filename: String::new(),
77                    halfpipe: false,
78                    tracks: false,
79                    author: String::new(),
80                    track: String::new(),
81                    volume: 0.3,
82                };
83            } else if in_music_block && let Some(equals_pos) = line.find('=') {
84                let key = line[..equals_pos].trim();
85                let value = line[equals_pos + 1..].trim().trim_end_matches(';').trim();
86
87                match key {
88                    "filename" => current_config.filename = value.trim_matches('"').to_string(),
89                    "halfpipe" => current_config.halfpipe = value == "1",
90                    "tracks" => current_config.tracks = value == "1",
91                    "author" => current_config.author = value.trim_matches('"').to_string(),
92                    "track" => current_config.track = value.trim_matches('"').to_string(),
93                    "volume" => current_config.volume = value.parse().unwrap_or(0.3),
94                    _ => {}
95                }
96            }
97        }
98    }
99
100    configs
101}
102
103pub fn save_music_config(
104    music_dir: &Path,
105    configs: &[MusicConfig],
106) -> Result<(), Box<dyn std::error::Error>> {
107    let music_config_path = music_dir.join("music_config.txt");
108    let mut file = fs::File::create(music_config_path)?;
109
110    // Get the absolute path of the music directory to construct proper paths
111    let absolute_music_path = music_dir.canonicalize()?;
112    let drive_root = if let Some(parent) = absolute_music_path.parent() {
113        if let Some(grandparent) = parent.parent() {
114            grandparent.to_path_buf()
115        } else {
116            parent.to_path_buf()
117        }
118    } else {
119        absolute_music_path.clone()
120    };
121
122    for config in configs {
123        writeln!(file, "Music {{")?;
124
125        // Convert the filename to an absolute path from drive root
126        let file_path = if config.filename.starts_with("data/music/") {
127            // This is already a relative path, convert to absolute from drive root
128            let relative_path = config.filename.trim_start_matches("data/music/");
129            let mut absolute_path = drive_root.join("data/music");
130            for part in relative_path.split('/') {
131                absolute_path = absolute_path.join(part);
132            }
133            absolute_path
134        } else {
135            // Try to interpret as absolute path
136            PathBuf::from(&config.filename)
137        };
138
139        // Convert to string and normalize path separators
140        let path_string = file_path.to_string_lossy().to_string();
141        let normalized_path = path_string.replace('\\', "/");
142
143        // Strip Windows \\?\ prefix if present
144        let cleaned = normalized_path.trim_start_matches("//?/");
145
146        // Strip drive letter (e.g. "C:/")
147        let cleaned = if cleaned.len() > 2 && cleaned.chars().nth(1) == Some(':') {
148            &cleaned[2..] // remove "C:"
149        } else {
150            cleaned
151        };
152
153        // Remove leading slash if present
154        let cleaned = cleaned.trim_start_matches('/');
155
156        writeln!(file, "    filename = \"{cleaned}\";")?;
157        writeln!(
158            file,
159            "    halfpipe = {};",
160            if config.halfpipe { "1" } else { "0" }
161        )?;
162        writeln!(
163            file,
164            "    tracks = {};",
165            if config.tracks { "1" } else { "0" }
166        )?;
167        writeln!(file, "    author = \"{}\";", config.author)?;
168        writeln!(file, "    track = \"{}\";", config.track)?;
169        writeln!(file, "    volume = {};", config.volume)?;
170        writeln!(file, "}};")?;
171        writeln!(file)?;
172    }
173
174    Ok(())
175}
176
177#[must_use]
178pub fn load_and_reconcile_music_configs(music_dir: &Path, game_path: &Path) -> Vec<MusicConfig> {
179    if !music_dir.exists() {
180        return Vec::new();
181    }
182
183    // Load existing configs
184    let configs = load_music_config(music_dir);
185
186    // Discover all ADP files and create a mapping by filename
187    let adp_files = discover_music_files(music_dir);
188
189    use std::collections::HashMap;
190    let mut adp_file_map = HashMap::new();
191    for adp_file in &adp_files {
192        if let Some(file_name) = adp_file.file_name().and_then(|n| n.to_str()) {
193            let relative_path = adp_file.strip_prefix(music_dir).unwrap_or(adp_file);
194            let full_path = format!("data/music/{}", relative_path.display());
195            adp_file_map.insert(file_name.to_string(), (adp_file.clone(), full_path));
196        }
197    }
198
199    // Reconcile configs with actual files
200    let mut final_configs = Vec::new();
201    let mut seen_filenames = HashSet::new();
202
203    // First pass: process existing configs and fix paths if needed
204    for mut config in configs {
205        // Extract filename from path
206        let filename = if let Some(last_slash) = config.filename.rfind('/') {
207            config.filename[last_slash + 1..].to_string()
208        } else {
209            config.filename.clone()
210        };
211
212        // Check if this filename already exists in our final configs
213        if seen_filenames.contains(&filename) {
214            continue; // Skip duplicates
215        }
216        seen_filenames.insert(filename.clone());
217
218        // Check if the file exists at the configured path
219        let configured_path = game_path.join(&config.filename);
220        let file_exists_at_configured_path = configured_path.exists();
221
222        // Check if the file exists anywhere in the music directory
223        if let Some((actual_file, correct_path)) = adp_file_map.get(&filename) {
224            let file_exists = actual_file.exists();
225
226            if file_exists {
227                if !file_exists_at_configured_path {
228                    // Path is broken but file exists - fix the path
229                    config.filename = correct_path.clone();
230                }
231                final_configs.push(config);
232            } else {
233                // File doesn't exist at all - keep the config but mark as missing
234                final_configs.push(config);
235            }
236        } else {
237            // No corresponding file found - keep the config but mark as missing
238            final_configs.push(config);
239        }
240    }
241
242    // Second pass: add configs for files that don't have existing configs
243    for (file_name, (_adp_file, full_path)) in &adp_file_map {
244        if !seen_filenames.contains(file_name) {
245            seen_filenames.insert(file_name.clone());
246
247            final_configs.push(MusicConfig {
248                filename: full_path.clone(),
249                halfpipe: true,
250                tracks: true,
251                author: "Unknown".to_string(),
252                track: file_name.trim_end_matches(".adp").replace('_', " ").clone(),
253                volume: 0.3,
254            });
255        }
256    }
257
258    final_configs
259}
260
261#[derive(Debug, Clone, PartialEq)]
262pub enum MusicFileStatus {
263    /// File exists at the configured path
264    Ok,
265    /// Path was automatically fixed
266    Fixed,
267    /// File not found at all
268    Missing,
269}
270
271#[must_use]
272pub fn get_music_file_status(config: &MusicConfig, game_path: &Path) -> MusicFileStatus {
273    // Convert relative path to absolute path
274    let configured_path = if config.filename.starts_with("data/music/") {
275        game_path.join(&config.filename)
276    } else {
277        game_path.join("data/music").join(&config.filename)
278    };
279
280    // Check if file exists at the configured path
281    let file_exists_at_configured_path = configured_path.exists();
282
283    // Check if file exists anywhere in the music directory (path was fixed)
284    let filename = if let Some(last_slash) = config.filename.rfind('/') {
285        &config.filename[last_slash + 1..]
286    } else {
287        &config.filename
288    };
289
290    let music_dir = game_path.join("data/music");
291    let file_exists_anywhere = music_dir.join(filename).exists();
292
293    // Return status enum
294    if file_exists_at_configured_path {
295        MusicFileStatus::Ok
296    } else if file_exists_anywhere {
297        MusicFileStatus::Fixed
298    } else {
299        MusicFileStatus::Missing
300    }
301}
302
303#[must_use]
304pub fn get_music_filename_only(config: &MusicConfig) -> String {
305    // Extract just the filename from the full path
306    if let Some(last_slash) = config.filename.rfind('/') {
307        config.filename[last_slash + 1..].to_string()
308    } else {
309        config.filename.clone()
310    }
311}
312
313pub fn generate_music_config_file(music_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
314    // Discover all music files
315    let music_files = discover_music_files(music_dir);
316
317    // Create default configs for each music file
318    let mut configs = Vec::new();
319
320    for music_file in music_files {
321        if let Some(file_name) = music_file.file_name().and_then(|n| n.to_str()) {
322            let relative_path = music_file.strip_prefix(music_dir).unwrap_or(&music_file);
323            let full_path = format!("data/music/{}", relative_path.display());
324
325            configs.push(MusicConfig {
326                filename: full_path,
327                halfpipe: true,
328                tracks: true,
329                author: String::new(),
330                track: file_name.trim_end_matches(".adp").replace('_', " ").clone(),
331                volume: 0.3,
332            });
333        }
334    }
335
336    // Save the config
337    save_music_config(music_dir, &configs)?;
338
339    Ok(())
340}