Skip to main content

supreme_boards/
lib.rs

1use std::collections::HashSet;
2use std::fs;
3use std::io::Write;
4use std::path::{Path, PathBuf};
5
6mod board_config;
7pub use board_config::{BoardConfig, parse_board_config, write_board_config};
8
9/// Generates a `board_types.txt` file listing all valid boards.
10///
11/// # Errors
12///
13/// Returns an error if the file cannot be created or written to.
14pub fn generate_board_types_file(boards_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
15    let board_types_path = boards_dir.join("board_types.txt");
16    let mut file = fs::File::create(&board_types_path)?;
17
18    // Get valid board directories (those with boardcnf.txt)
19    let valid_boards = get_valid_board_directories(boards_dir);
20
21    // Write board entries
22    for board_dir in valid_boards {
23        if let Some(board_name) = board_dir.file_name().and_then(|n| n.to_str()) {
24            writeln!(
25                file,
26                "board\n{{\n\tboard_config_file = \"data/boards/{board_name}/boardcnf.txt\";\n}};",
27            )?;
28        }
29    }
30
31    Ok(())
32}
33
34#[must_use]
35pub fn get_valid_board_directories(boards_dir: &Path) -> Vec<PathBuf> {
36    let mut board_dirs = Vec::new();
37
38    if let Ok(entries) = fs::read_dir(boards_dir) {
39        for entry in entries.flatten() {
40            let path = entry.path();
41            if path.is_dir() && board_has_config(&path) {
42                board_dirs.push(path);
43            }
44        }
45    }
46
47    // Natural sort the board directories
48    natural_sort(&mut board_dirs);
49    board_dirs
50}
51
52#[must_use]
53pub fn board_has_config(board_dir: &Path) -> bool {
54    let board_config_path = board_dir.join("boardcnf.txt");
55    board_config_path.exists()
56}
57
58#[must_use]
59pub fn get_available_boards(game_path: &Path) -> std::collections::HashSet<String> {
60    let available_boards_path = game_path.join("Saved_Data/available_boards.txt");
61    let mut available = std::collections::HashSet::new();
62
63    if let Ok(content) = fs::read_to_string(available_boards_path) {
64        for line in content.lines() {
65            let line = line.trim();
66            if line.is_empty() {
67                continue;
68            }
69
70            // Parse the format: "board_name",status,
71            if let Some(comma_pos) = line.find(',') {
72                let board_part = &line[..comma_pos];
73                let board_name = board_part.trim_matches('"');
74
75                // Get the rest of the line after the first comma
76                let rest = &line[comma_pos + 1..];
77
78                // Find the status (could be followed by another comma or end of line)
79                let status_part = if let Some(second_comma_pos) = rest.find(',') {
80                    &rest[..second_comma_pos]
81                } else {
82                    rest.trim()
83                };
84
85                // Check if status is 1 (available)
86                if status_part.trim() == "1" {
87                    available.insert(board_name.to_string());
88                }
89            }
90        }
91    }
92
93    available
94}
95
96/// Sets whether a board is available in the game.
97///
98/// # Errors
99///
100/// Returns an error if the available boards file cannot be read or updated.
101pub fn set_board_available(
102    game_path: &Path,
103    board_name: &str,
104    available: bool,
105) -> Result<(), Box<dyn std::error::Error>> {
106    let available_boards_path = game_path.join("Saved_Data/available_boards.txt");
107    let mut available_boards = get_available_boards(game_path);
108
109    if available {
110        available_boards.insert(board_name.to_string());
111    } else {
112        available_boards.remove(board_name);
113    }
114
115    // Get all valid boards to maintain the complete list
116    let boards_dir = game_path.join("data/boards");
117    let all_valid_boards = get_valid_board_directories(&boards_dir);
118
119    // Write using the shared function
120    write_available_boards_file(&available_boards_path, &available_boards, &all_valid_boards)?;
121
122    Ok(())
123}
124
125/// Generates all necessary board configuration files for the game.
126///
127/// # Errors
128///
129/// Returns an error if any of the configuration files cannot be generated.
130pub fn generate_all_boards_files(
131    boards_dir: &Path,
132    game_path: &Path,
133) -> Result<(), Box<dyn std::error::Error>> {
134    // Always generate board_types.txt (this should reflect all available boards)
135    generate_board_types_file(boards_dir)?;
136
137    // Only generate available_boards.txt if it doesn't exist
138    let available_boards_path = game_path.join("Saved_Data/available_boards.txt");
139
140    if !available_boards_path.exists() {
141        // Generate available_boards.txt with all valid boards enabled by default
142        let valid_boards = get_valid_board_directories(boards_dir);
143
144        // Create Saved_Data directory if it doesn't exist
145        if let Some(parent) = available_boards_path.parent() {
146            fs::create_dir_all(parent)?;
147        }
148
149        // Create a set of all valid boards (all enabled by default)
150        let mut available_boards = HashSet::new();
151        for board_dir in &valid_boards {
152            if let Some(board_name) = board_dir.file_name().and_then(|n| n.to_str()) {
153                available_boards.insert(board_name.to_string());
154            }
155        }
156
157        // Write using the shared function
158        write_available_boards_file(&available_boards_path, &available_boards, &valid_boards)?;
159    }
160
161    Ok(())
162}
163
164fn write_available_boards_file(
165    available_boards_path: &Path,
166    available_boards: &HashSet<String>,
167    all_boards: &[PathBuf],
168) -> Result<(), Box<dyn std::error::Error>> {
169    // Create parent directory if it doesn't exist
170    if let Some(parent) = available_boards_path.parent() {
171        fs::create_dir_all(parent)?;
172    }
173
174    let mut file = fs::File::create(available_boards_path)?;
175
176    writeln!(file, "{{")?;
177
178    // Write all valid boards with their status
179    for (index, board_dir) in all_boards.iter().enumerate() {
180        if let Some(board_name) = board_dir.file_name().and_then(|n| n.to_str()) {
181            let status = if available_boards.contains(board_name) {
182                "1"
183            } else {
184                "0"
185            };
186            if index > 0 {
187                writeln!(file, ",")?;
188            }
189            write!(file, "    \"{board_name}\",{status}")?;
190        }
191    }
192
193    writeln!(file, "\n}}")?;
194
195    Ok(())
196}
197
198fn natural_sort(paths: &mut [PathBuf]) {
199    paths.sort_by(|a, b| {
200        let a_name = a.file_name().and_then(|n| n.to_str()).unwrap_or("");
201        let b_name = b.file_name().and_then(|n| n.to_str()).unwrap_or("");
202
203        // Extract numeric parts for natural sorting
204        let a_num = extract_number(a_name);
205        let b_num = extract_number(b_name);
206
207        a_num.cmp(&b_num)
208    });
209}
210
211fn extract_number(s: &str) -> u32 {
212    s.trim_start_matches("board_").parse::<u32>().unwrap_or(0)
213}
214
215#[derive(serde::Deserialize)]
216struct BoardRawConfig {
217    mat_file: String,
218}
219
220#[derive(serde::Deserialize)]
221struct MatRawConfig {
222    material: MaterialData,
223}
224
225#[derive(serde::Deserialize)]
226struct MaterialData {
227    texture: String,
228}
229
230/// Get the texture path from a board's configuration files.
231/// 1. Reads `boardcnf.txt` to find material file
232/// 2. Reads the material file to find the texture field
233#[must_use]
234pub fn get_board_texture_path(board_dir: &Path) -> Option<PathBuf> {
235    let board_config_path = board_dir.join("boardcnf.txt");
236    if !board_config_path.exists() {
237        return None;
238    }
239
240    let config: BoardRawConfig = match supreme_parsing::from_path(&board_config_path) {
241        Ok(c) => c,
242        Err(e) => {
243            eprintln!(
244                "Error parsing board config {}: {}",
245                board_config_path.display(),
246                e
247            );
248            return None;
249        }
250    };
251    let mat_file_val = &config.mat_file;
252
253    // The material file might be a relative path like "data/boards/board_2/board_mat.txt"
254    // or just a filename like "board_mat.txt".
255    let mat_file_path = if mat_file_val.contains('/') || mat_file_val.contains('\\') {
256        // It's a path. Extract the filename to find it in the board directory.
257        let p = Path::new(mat_file_val);
258        if let Some(filename) = p.file_name() {
259            board_dir.join(filename)
260        } else {
261            board_dir.join(mat_file_val)
262        }
263    } else {
264        board_dir.join(mat_file_val)
265    };
266
267    if !mat_file_path.exists() {
268        eprintln!("Material file not found: {}", mat_file_path.display());
269        return None;
270    }
271
272    let mat_map: MatRawConfig = match supreme_parsing::from_path(&mat_file_path) {
273        Ok(m) => m,
274        Err(e) => {
275            eprintln!(
276                "Error parsing material file {}: {}",
277                mat_file_path.display(),
278                e
279            );
280            return None;
281        }
282    };
283    Some(PathBuf::from(mat_map.material.texture))
284}