1use std::collections::BTreeSet;
8use std::path::{Path, PathBuf};
9
10use rakata_core::ResourceType;
11use rakata_formats::key::KeyResourceEntry;
12use rakata_formats::rim::{read_rim_from_bytes, RimResource};
13
14use crate::chitin::ChitinError;
15use crate::keyfile::KeyFileError;
16use crate::Chitin;
17
18pub type ResourcePair = (Vec<u8>, Option<Vec<u8>>, String);
20
21#[derive(Debug, thiserror::Error)]
23pub enum GameResourcesError {
24 #[error(transparent)]
26 Chitin(#[from] ChitinError),
27 #[error(transparent)]
29 KeyFile(#[from] KeyFileError),
30 #[error("I/O error for `{path}`: {source}")]
32 Io {
33 path: PathBuf,
35 source: std::io::Error,
37 },
38 #[error("RIM parse error for `{path}`: {source}")]
40 Rim {
41 path: PathBuf,
43 source: rakata_formats::rim::RimBinaryError,
45 },
46}
47
48pub struct GameResources {
69 chitin: Chitin,
70 rim_paths: Vec<PathBuf>,
71}
72
73impl GameResources {
74 pub fn open(game_root: impl AsRef<Path>) -> Result<Self, GameResourcesError> {
79 let game_root = game_root.as_ref();
80 let chitin = Chitin::read_from_root(game_root)?;
81
82 let modules_dir = game_root.join("modules");
83 let rim_paths = enumerate_rim_files(&modules_dir)?;
84
85 Ok(Self { chitin, rim_paths })
86 }
87
88 pub fn exists(
90 &self,
91 resref: &str,
92 resource_type: ResourceType,
93 ) -> Result<bool, GameResourcesError> {
94 Ok(self.find(resref, resource_type)?.is_some())
95 }
96
97 pub fn find(
102 &self,
103 resref: &str,
104 resource_type: ResourceType,
105 ) -> Result<Option<(Vec<u8>, String)>, GameResourcesError> {
106 let type_code = resource_type.into();
107
108 for rim_path in &self.rim_paths {
110 let rim = read_rim_file(rim_path)?;
111 let resref_parsed = resref.parse().unwrap_or_default();
112 if let Some(data) = rim.resource(&resref_parsed, type_code) {
113 let label = rim_label(rim_path);
114 return Ok(Some((data.to_vec(), label)));
115 }
116 }
117
118 if let Some(data) = self
120 .chitin
121 .key_file()
122 .resource(&resref.parse().unwrap_or_default(), type_code)?
123 {
124 return Ok(Some((data, "KEY/BIF".into())));
125 }
126
127 Ok(None)
128 }
129
130 pub fn find_pair(
136 &self,
137 resref: &str,
138 primary_type: ResourceType,
139 companion_type: ResourceType,
140 ) -> Result<Option<ResourcePair>, GameResourcesError> {
141 let primary_code = primary_type.into();
142 let companion_code = companion_type.into();
143
144 for rim_path in &self.rim_paths {
146 let rim = read_rim_file(rim_path)?;
147 let resref_parsed = resref.parse().unwrap_or_default();
148 if let Some(primary_data) = rim.resource(&resref_parsed, primary_code) {
149 let companion_data = rim
150 .resource(&resref_parsed, companion_code)
151 .map(|d| d.to_vec());
152 let label = rim_label(rim_path);
153 return Ok(Some((primary_data.to_vec(), companion_data, label)));
154 }
155 }
156
157 let resref_parsed = resref.parse().unwrap_or_default();
159 if let Some(primary_data) = self
160 .chitin
161 .key_file()
162 .resource(&resref_parsed, primary_code)?
163 {
164 let companion_data = self
165 .chitin
166 .key_file()
167 .resource(&resref_parsed, companion_code)?;
168 return Ok(Some((primary_data, companion_data, "KEY/BIF".into())));
169 }
170
171 Ok(None)
172 }
173
174 pub fn for_each<F>(
180 &self,
181 resource_type: ResourceType,
182 mut f: F,
183 ) -> Result<(), GameResourcesError>
184 where
185 F: FnMut(&str, Vec<u8>, &str),
186 {
187 let type_code = resource_type.into();
188 let mut seen: BTreeSet<String> = BTreeSet::new();
189
190 for rim_path in &self.rim_paths {
192 let rim = read_rim_file(rim_path)?;
193 let label = rim_label(rim_path);
194 for res in &rim.resources {
195 if res.resource_type == type_code {
196 let key = res.resref.to_string();
197 if !seen.contains(&key) {
198 f(&key, res.data.clone(), &label);
199 seen.insert(key);
200 }
201 }
202 }
203 }
204
205 for_each_key_resource(
207 &self.chitin,
208 type_code,
209 &mut seen,
210 &mut |resref, data, label| f(resref, data, label),
211 )?;
212
213 Ok(())
214 }
215
216 pub fn for_each_pair<F>(
221 &self,
222 primary_type: ResourceType,
223 companion_type: ResourceType,
224 mut f: F,
225 ) -> Result<(), GameResourcesError>
226 where
227 F: FnMut(&str, Vec<u8>, Option<Vec<u8>>, &str),
228 {
229 let primary_code = primary_type.into();
230 let companion_code = companion_type.into();
231 let mut seen: BTreeSet<String> = BTreeSet::new();
232
233 for rim_path in &self.rim_paths {
235 let rim = read_rim_file(rim_path)?;
236 let label = rim_label(rim_path);
237 for_each_pair_in_rim(
238 &rim,
239 primary_code,
240 companion_code,
241 &label,
242 &mut seen,
243 &mut f,
244 );
245 }
246
247 for_each_key_resource_pair(
249 &self.chitin,
250 primary_code,
251 companion_code,
252 &mut seen,
253 &mut |resref, data, companion, label| f(resref, data, companion, label),
254 )?;
255
256 Ok(())
257 }
258}
259
260fn enumerate_rim_files(modules_dir: &Path) -> Result<Vec<PathBuf>, GameResourcesError> {
266 let entries = match std::fs::read_dir(modules_dir) {
267 Ok(entries) => entries,
268 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
269 Err(e) => {
270 return Err(GameResourcesError::Io {
271 path: modules_dir.to_path_buf(),
272 source: e,
273 })
274 }
275 };
276
277 let mut paths: Vec<PathBuf> = entries
278 .filter_map(|entry| {
279 let path = entry.ok()?.path();
280 let ext = path.extension()?.to_str()?;
281 if ext.eq_ignore_ascii_case("rim") {
282 Some(path)
283 } else {
284 None
285 }
286 })
287 .collect();
288
289 paths.sort();
290 Ok(paths)
291}
292
293fn read_rim_file(path: &Path) -> Result<rakata_formats::rim::Rim, GameResourcesError> {
295 let bytes = std::fs::read(path).map_err(|e| GameResourcesError::Io {
296 path: path.to_path_buf(),
297 source: e,
298 })?;
299 read_rim_from_bytes(&bytes).map_err(|e| GameResourcesError::Rim {
300 path: path.to_path_buf(),
301 source: e,
302 })
303}
304
305fn rim_label(path: &Path) -> String {
307 path.file_name()
308 .and_then(|n| n.to_str())
309 .unwrap_or("unknown")
310 .to_string()
311}
312
313fn for_each_pair_in_rim(
315 rim: &rakata_formats::rim::Rim,
316 primary_code: rakata_core::ResourceTypeCode,
317 companion_code: rakata_core::ResourceTypeCode,
318 label: &str,
319 seen: &mut BTreeSet<String>,
320 f: &mut impl FnMut(&str, Vec<u8>, Option<Vec<u8>>, &str),
321) {
322 let primaries: Vec<&RimResource> = rim
324 .resources
325 .iter()
326 .filter(|r| r.resource_type == primary_code)
327 .collect();
328
329 for primary in primaries {
330 let key = primary.resref.to_string();
331 if !seen.insert(key.clone()) {
332 continue;
333 }
334 let companion = rim
335 .resource(&primary.resref, companion_code)
336 .map(|d| d.to_vec());
337 f(&key, primary.data.clone(), companion, label);
338 }
339}
340
341fn for_each_key_resource(
343 chitin: &Chitin,
344 type_code: rakata_core::ResourceTypeCode,
345 seen: &mut BTreeSet<String>,
346 f: &mut impl FnMut(&str, Vec<u8>, &str),
347) -> Result<(), GameResourcesError> {
348 let key = chitin.key();
349 let entries: Vec<&KeyResourceEntry> = key
350 .resources
351 .iter()
352 .filter(|e| e.resource_type == type_code)
353 .collect();
354
355 for entry in entries {
356 let lc = entry.resref.to_string();
357 if !seen.insert(lc.clone()) {
358 continue;
359 }
360 let data = chitin
361 .key_file()
362 .read_resource_by_seek(entry.resource_id.bif_index(), entry.resource_id)?;
363 f(&lc, data, "KEY/BIF");
364 }
365
366 Ok(())
367}
368
369fn for_each_key_resource_pair(
371 chitin: &Chitin,
372 primary_code: rakata_core::ResourceTypeCode,
373 companion_code: rakata_core::ResourceTypeCode,
374 seen: &mut BTreeSet<String>,
375 f: &mut impl FnMut(&str, Vec<u8>, Option<Vec<u8>>, &str),
376) -> Result<(), GameResourcesError> {
377 let key = chitin.key();
378 let entries: Vec<&KeyResourceEntry> = key
379 .resources
380 .iter()
381 .filter(|e| e.resource_type == primary_code)
382 .collect();
383
384 for entry in entries {
385 let lc = entry.resref.to_string();
386 if !seen.insert(lc.clone()) {
387 continue;
388 }
389 let data = chitin
390 .key_file()
391 .read_resource_by_seek(entry.resource_id.bif_index(), entry.resource_id)?;
392
393 let companion = chitin.key_file().resource(&entry.resref, companion_code)?;
395
396 f(&lc, data, companion, "KEY/BIF");
397 }
398
399 Ok(())
400}