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 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 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 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 let file_path = if config.filename.starts_with("data/music/") {
127 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 PathBuf::from(&config.filename)
137 };
138
139 let path_string = file_path.to_string_lossy().to_string();
141 let normalized_path = path_string.replace('\\', "/");
142
143 let cleaned = normalized_path.trim_start_matches("//?/");
145
146 let cleaned = if cleaned.len() > 2 && cleaned.chars().nth(1) == Some(':') {
148 &cleaned[2..] } else {
150 cleaned
151 };
152
153 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 let configs = load_music_config(music_dir);
185
186 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 let mut final_configs = Vec::new();
201 let mut seen_filenames = HashSet::new();
202
203 for mut config in configs {
205 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 if seen_filenames.contains(&filename) {
214 continue; }
216 seen_filenames.insert(filename.clone());
217
218 let configured_path = game_path.join(&config.filename);
220 let file_exists_at_configured_path = configured_path.exists();
221
222 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 config.filename = correct_path.clone();
230 }
231 final_configs.push(config);
232 } else {
233 final_configs.push(config);
235 }
236 } else {
237 final_configs.push(config);
239 }
240 }
241
242 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 Ok,
265 Fixed,
267 Missing,
269}
270
271#[must_use]
272pub fn get_music_file_status(config: &MusicConfig, game_path: &Path) -> MusicFileStatus {
273 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 let file_exists_at_configured_path = configured_path.exists();
282
283 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 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 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 let music_files = discover_music_files(music_dir);
316
317 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_music_config(music_dir, &configs)?;
338
339 Ok(())
340}