brawllib_rs/
brawl_mod.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use crate::arc;
5use crate::fighter::Fighter;
6use crate::wii_memory::WiiMemory;
7use crate::wiird_runner;
8
9use anyhow::{Error, bail};
10
11use fancy_slice::FancySlice;
12
13/// This is very cheap to create, it just contains the passed paths.
14/// All the actual work is done in the `load_*` methods.
15pub struct BrawlMod {
16    brawl_path: PathBuf,
17    mod_path: Option<PathBuf>,
18}
19
20impl BrawlMod {
21    /// This is the main entry point of the library.
22    /// Provide the path to a brawl dump and optionally a brawl mod sd card.
23    ///
24    /// Then you can load various other structs from the BrawlMod methods.
25    pub fn new(brawl_path: &Path, mod_path: Option<&Path>) -> BrawlMod {
26        BrawlMod {
27            brawl_path: brawl_path.to_path_buf(),
28            mod_path: mod_path.map(|x| x.to_path_buf()),
29        }
30    }
31
32    /// Returns Err(..) on failure to read required files from disk.
33    /// Fighter specific missing files and errors encountered when parsing data is reported via the `error!()` macro from the log crate.
34    /// You will need to use one of these crates to view the logged errors <https://github.com/rust-lang-nursery/log#in-executables>
35    pub fn load_fighters(&self, single_model: bool) -> Result<Vec<Fighter>, Error> {
36        let brawl_fighter_path = self.brawl_path.join("fighter");
37        let brawl_fighter_dir = match fs::read_dir(&brawl_fighter_path) {
38            Ok(dir) => dir,
39            Err(err) => bail!("Cannot read fighter directory in the brawl dump: {}", err),
40        };
41
42        let mut mod_fighter_dir = None;
43        if let Some(mod_path) = &self.mod_path {
44            let dir_reader = match fs::read_dir(mod_path) {
45                Ok(dir) => dir,
46                Err(err) => bail!("Cannot read brawl mod directory: {}", err),
47            };
48
49            for dir in dir_reader.flatten() {
50                let path = dir.path().join("pf/fighter");
51                if path.exists() {
52                    mod_fighter_dir = Some(match fs::read_dir(path) {
53                        Ok(dir) => dir,
54                        Err(err) => {
55                            bail!("Cannot read fighter directory in the brawl mod: {}", err)
56                        }
57                    });
58                    break;
59                }
60            }
61        }
62        if self.mod_path.is_some() && mod_fighter_dir.is_none() {
63            bail!("Missing mod_name/pf/fighter directory");
64        }
65
66        let common_fighter_path = brawl_fighter_path.join("Fighter.pac");
67        let (common_fighter, wii_memory) =
68            if let Ok(mut file_data) = std::fs::read(common_fighter_path) {
69                let wii_memory = if self.mod_path.is_some() {
70                    let codeset = self.load_wiird_codeset_raw()?;
71                    let sakurai_ram_offset = 0x80F9FC20;
72                    let sakurai_fighter_pac_offset = 0x80;
73                    let fighter_pac_offset = sakurai_ram_offset - sakurai_fighter_pac_offset;
74
75                    wiird_runner::process(&codeset, &mut file_data, fighter_pac_offset)
76                } else {
77                    WiiMemory::new()
78                };
79
80                let data = FancySlice::new(&file_data);
81
82                (arc::arc(data, &wii_memory, false), wii_memory)
83            } else {
84                bail!("Missing Fighter.pac");
85            };
86
87        Ok(Fighter::load(
88            brawl_fighter_dir,
89            mod_fighter_dir,
90            &common_fighter,
91            &wii_memory,
92            single_model,
93        ))
94    }
95
96    pub fn load_wiird_codeset_raw(&self) -> Result<Vec<u8>, Error> {
97        // RSBE01.gct is usually located in the codes folder but can also be in the main sub folder e.g. LXP 2.1
98        // Additionally P+ now has a second codeset file called BOOST.GCT
99        // So we will load every *.gct file from within every subdirectory of the root.
100        // If we have already read a file of the same name, we assert the contents are equal, then skip it.
101        // Once all files are loaded, strip the headers and concatenate them in a deterministic fashion.
102
103        struct GCTFile {
104            pub name: String,
105            pub data: Vec<u8>,
106        }
107
108        let mut gct_files: Vec<GCTFile> = vec![];
109        if let Some(mod_path) = &self.mod_path {
110            for entry in fs::read_dir(mod_path).unwrap().flatten() {
111                if entry.path().is_dir() {
112                    let child_dir = entry.path();
113                    for entry in fs::read_dir(child_dir).unwrap().flatten() {
114                        let name = entry.file_name().into_string().unwrap();
115                        if name.ends_with(".gct") || name.ends_with(".GCT") {
116                            let codeset_path = entry.path();
117                            if codeset_path.exists() {
118                                match std::fs::read(&codeset_path) {
119                                    Ok(data) => {
120                                        if data.len() < 8 {
121                                            bail!(
122                                                "Not a WiiRD gct codeset file: File size is less than 8 bytes"
123                                            );
124                                        }
125                                        if let Some(matching_file) =
126                                            gct_files.iter().find(|x| x.name == name)
127                                        {
128                                            assert_eq!(matching_file.data, data);
129                                        } else {
130                                            gct_files.push(GCTFile { name, data });
131                                        }
132                                    }
133                                    Err(err) => bail!(
134                                        "Cannot read WiiRD codeset {:?}: {}",
135                                        codeset_path,
136                                        err
137                                    ),
138                                };
139                            }
140                        }
141                    }
142                }
143            }
144        } else {
145            bail!("Not a mod, vanilla brawl does not have a WiiRD codeset.");
146        }
147
148        // Very important that the resulting wiird_codeset is deterministic
149        // Will make issues much easier to reproduce.
150        gct_files.sort_by_key(|x| x.name.clone());
151
152        let mut result = vec![];
153        for gct_file in gct_files {
154            result.extend(&gct_file.data[8..]); // skip the header
155        }
156        Ok(result)
157    }
158
159    /// returns true if modded files are used.
160    /// Otherwise is just vanilla brawl and false is returned.
161    pub fn is_mod(&self) -> bool {
162        self.mod_path.is_some()
163    }
164}