1use std::collections::HashMap;
7use std::fs;
8use std::path::{Path, PathBuf};
9
10use crate::error::{M2Error, Result};
11
12pub trait FileResolver {
14 fn resolve_file_data_id(&self, id: u32) -> Result<String>;
16
17 fn load_file_by_id(&self, id: u32) -> Result<Vec<u8>>;
19
20 fn load_skin_by_id(&self, id: u32) -> Result<Vec<u8>> {
22 self.load_file_by_id(id)
23 }
24
25 fn load_animation_by_id(&self, id: u32) -> Result<Vec<u8>> {
27 self.load_file_by_id(id)
28 }
29
30 fn load_texture_by_id(&self, id: u32) -> Result<Vec<u8>> {
32 self.load_file_by_id(id)
33 }
34
35 fn load_physics_by_id(&self, id: &u32) -> Result<Vec<u8>> {
37 self.load_file_by_id(*id)
38 }
39
40 fn load_skeleton_by_id(&self, id: &u32) -> Result<Vec<u8>> {
42 self.load_file_by_id(*id)
43 }
44
45 fn load_bone_by_id(&self, id: &u32) -> Result<Vec<u8>> {
47 self.load_file_by_id(*id)
48 }
49}
50
51#[derive(Debug)]
54pub struct ListfileResolver {
55 id_to_path: HashMap<u32, String>,
57 base_path: Option<PathBuf>,
59}
60
61impl ListfileResolver {
62 pub fn new() -> Self {
64 Self {
65 id_to_path: HashMap::new(),
66 base_path: None,
67 }
68 }
69
70 pub fn with_base_path<P: AsRef<Path>>(base_path: P) -> Self {
72 Self {
73 id_to_path: HashMap::new(),
74 base_path: Some(base_path.as_ref().to_path_buf()),
75 }
76 }
77
78 pub fn load_from_csv<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
80 let contents = fs::read_to_string(path)
81 .map_err(|e| M2Error::ExternalFileError(format!("Failed to read listfile: {}", e)))?;
82
83 for line in contents.lines() {
84 let line = line.trim();
85 if line.is_empty() || line.starts_with('#') {
86 continue;
87 }
88
89 let parts: Vec<&str> = line.split(';').collect();
91 if parts.len() >= 2
92 && let Ok(id) = parts[0].parse::<u32>()
93 {
94 let path = parts[1].to_string();
95 self.id_to_path.insert(id, path);
96 }
97 }
98
99 Ok(())
100 }
101
102 pub fn load_from_text<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
104 let contents = fs::read_to_string(path)
105 .map_err(|e| M2Error::ExternalFileError(format!("Failed to read listfile: {}", e)))?;
106
107 for line in contents.lines() {
108 let line = line.trim();
109 if line.is_empty() || line.starts_with('#') {
110 continue;
111 }
112
113 let parts: Vec<&str> = line.split_whitespace().collect();
115 if parts.len() >= 2
116 && let Ok(id) = parts[0].parse::<u32>()
117 {
118 let path = parts[1..].join(" "); self.id_to_path.insert(id, path);
120 }
121 }
122
123 Ok(())
124 }
125
126 pub fn add_mapping<S: Into<String>>(&mut self, id: u32, path: S) {
128 self.id_to_path.insert(id, path.into());
129 }
130
131 pub fn remove_mapping(&mut self, id: u32) -> Option<String> {
133 self.id_to_path.remove(&id)
134 }
135
136 pub fn len(&self) -> usize {
138 self.id_to_path.len()
139 }
140
141 pub fn is_empty(&self) -> bool {
143 self.id_to_path.is_empty()
144 }
145
146 pub fn set_base_path<P: AsRef<Path>>(&mut self, base_path: P) {
148 self.base_path = Some(base_path.as_ref().to_path_buf());
149 }
150
151 fn resolve_path(&self, file_path: &str) -> PathBuf {
153 match &self.base_path {
154 Some(base) => base.join(file_path),
155 None => PathBuf::from(file_path),
156 }
157 }
158}
159
160impl Default for ListfileResolver {
161 fn default() -> Self {
162 Self::new()
163 }
164}
165
166impl FileResolver for ListfileResolver {
167 fn resolve_file_data_id(&self, id: u32) -> Result<String> {
168 self.id_to_path
169 .get(&id)
170 .cloned()
171 .ok_or(M2Error::UnknownFileDataId(id))
172 }
173
174 fn load_file_by_id(&self, id: u32) -> Result<Vec<u8>> {
175 let file_path = self.resolve_file_data_id(id)?;
176 let absolute_path = self.resolve_path(&file_path);
177
178 fs::read(&absolute_path).map_err(|e| {
179 M2Error::ExternalFileError(format!(
180 "Failed to load file {} (ID {}): {}",
181 absolute_path.display(),
182 id,
183 e
184 ))
185 })
186 }
187}
188
189#[derive(Debug)]
192pub struct PathResolver {
193 base_path: PathBuf,
194}
195
196impl PathResolver {
197 pub fn new<P: AsRef<Path>>(base_path: P) -> Self {
199 Self {
200 base_path: base_path.as_ref().to_path_buf(),
201 }
202 }
203
204 pub fn load_file<P: AsRef<Path>>(&self, path: P) -> Result<Vec<u8>> {
206 let full_path = self.base_path.join(path.as_ref());
207 fs::read(&full_path).map_err(|e| {
208 M2Error::ExternalFileError(format!(
209 "Failed to load file {}: {}",
210 full_path.display(),
211 e
212 ))
213 })
214 }
215}
216
217impl FileResolver for PathResolver {
218 fn resolve_file_data_id(&self, id: u32) -> Result<String> {
219 Err(M2Error::UnknownFileDataId(id))
221 }
222
223 fn load_file_by_id(&self, id: u32) -> Result<Vec<u8>> {
224 Err(M2Error::UnknownFileDataId(id))
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 use std::io::Write;
233 use tempfile::NamedTempFile;
234
235 #[test]
236 fn test_listfile_resolver_csv_format() {
237 let mut resolver = ListfileResolver::new();
238
239 let mut listfile = NamedTempFile::new().unwrap();
241 writeln!(listfile, "# Comment line").unwrap();
242 writeln!(listfile, "123456;World\\Maps\\Azeroth\\Azeroth.wdt").unwrap();
243 writeln!(listfile, "789012;Creature\\Human\\Male\\HumanMale.m2").unwrap();
244 writeln!(listfile).unwrap(); listfile.flush().unwrap();
246
247 resolver.load_from_csv(listfile.path()).unwrap();
248
249 assert_eq!(resolver.len(), 2);
250 assert_eq!(
251 resolver.resolve_file_data_id(123456).unwrap(),
252 "World\\Maps\\Azeroth\\Azeroth.wdt"
253 );
254 assert_eq!(
255 resolver.resolve_file_data_id(789012).unwrap(),
256 "Creature\\Human\\Male\\HumanMale.m2"
257 );
258
259 assert!(resolver.resolve_file_data_id(999999).is_err());
261 }
262
263 #[test]
264 fn test_listfile_resolver_text_format() {
265 let mut resolver = ListfileResolver::new();
266
267 let mut listfile = NamedTempFile::new().unwrap();
269 writeln!(listfile, "# Comment line").unwrap();
270 writeln!(listfile, "123456 World/Maps/Azeroth/Azeroth.wdt").unwrap();
271 writeln!(listfile, "789012 Creature/Human/Male/HumanMale.m2").unwrap();
272 writeln!(listfile, "555666 Path with spaces/file.blp").unwrap();
273 listfile.flush().unwrap();
274
275 resolver.load_from_text(listfile.path()).unwrap();
276
277 assert_eq!(resolver.len(), 3);
278 assert_eq!(
279 resolver.resolve_file_data_id(555666).unwrap(),
280 "Path with spaces/file.blp"
281 );
282 }
283
284 #[test]
285 fn test_listfile_resolver_manual_mappings() {
286 let mut resolver = ListfileResolver::new();
287
288 resolver.add_mapping(12345, "test/file.m2");
289 resolver.add_mapping(67890, "another/file.blp");
290
291 assert_eq!(resolver.len(), 2);
292 assert_eq!(
293 resolver.resolve_file_data_id(12345).unwrap(),
294 "test/file.m2"
295 );
296
297 let removed = resolver.remove_mapping(12345);
298 assert_eq!(removed, Some("test/file.m2".to_string()));
299 assert_eq!(resolver.len(), 1);
300 }
301
302 #[test]
303 fn test_path_resolver() {
304 let temp_dir = tempfile::tempdir().unwrap();
305 let resolver = PathResolver::new(temp_dir.path());
306
307 assert!(resolver.resolve_file_data_id(123).is_err());
309 assert!(resolver.load_file_by_id(123).is_err());
310
311 let test_file_path = temp_dir.path().join("test.txt");
313 fs::write(&test_file_path, b"test content").unwrap();
314
315 let content = resolver.load_file("test.txt").unwrap();
316 assert_eq!(content, b"test content");
317 }
318}