Skip to main content

lib_bcsv_jmap/
hash.rs

1use std::collections::HashMap;
2use std::fs::File;
3use std::io::{BufRead, BufReader};
4use std::path::Path;
5
6use crate::error::{JMapError, Result};
7
8/// The hash function used by Super Mario Galaxy 1
9///
10/// # Arguments
11/// - `field_name` - The ASCII field name to hash
12///
13/// # Returns
14/// A 32-bit hash value
15pub fn calc_hash(field_name: &str) -> u32 {
16    let mut hash: u32 = 0;
17
18    for byte in field_name.bytes() {
19        let ch = if byte & 0x80 != 0 {
20            byte as i8 as i32
21        } else {
22            byte as i32
23        };
24
25        hash = hash.wrapping_mul(31).wrapping_add(ch as u32);
26    }
27
28    hash
29}
30
31/// Trait for hash table implementations
32pub trait HashTable {
33    /// Calculate the hash for a field name
34    ///
35    /// # Arguments
36    /// - `field_name` - The field name to hash
37    ///
38    /// # Returns
39    /// The hash value corresponding to the given field name
40    fn calc(&self, field_name: &str) -> u32;
41
42    /// Find the field name for a given hash
43    /// Returns a hex representation like `[DEADBEEF]` if not found
44    /// # Arguments
45    /// - `hash` - The hash value to look up
46    ///
47    /// # Returns
48    /// The field name corresponding to the given hash, or a hex string if not found
49    fn find(&self, hash: u32) -> String;
50
51    /// Add a field name to the lookup table and return its hash
52    ///
53    /// # Arguments
54    /// - `field_name` - The field name to add to the lookup table
55    ///
56    /// # Returns
57    /// The hash value corresponding to the added field name
58    fn add(&mut self, field_name: &str) -> u32;
59}
60
61/// Type of hash algorithm to use
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum HashAlgorithm {
64    SMG,
65}
66
67impl HashAlgorithm {
68    /// Calculate hash using this algorithm
69    ///
70    /// # Arguments
71    /// - `field_name` - The field name to hash
72    ///
73    /// # Returns
74    /// The calculated hash value base of the hash algorithm
75    pub fn calc(&self, field_name: &str) -> u32 {
76        match self {
77            HashAlgorithm::SMG => calc_hash(field_name),
78        }
79    }
80}
81
82/// A hash lookup table backed by a file of known field names
83#[derive(Debug, Clone)]
84pub struct FileHashTable {
85    algorithm: HashAlgorithm,
86    lookup: HashMap<u32, String>,
87}
88
89impl FileHashTable {
90    /// Create a new empty hash table with the given algorithm
91    ///
92    /// # Arguments
93    /// - `algorithm` - The hash algorithm to use for calculating hashes
94    ///
95    /// # Returns
96    /// A new `FileHashTable` instance with the specified algorithm and an empty lookup table
97    pub fn new(algorithm: HashAlgorithm) -> Self {
98        Self {
99            algorithm,
100            lookup: HashMap::new(),
101        }
102    }
103
104    /// Create a new hash table with the given algorithm and lookup file
105    ///
106    /// The lookup file should contain one field name per line
107    /// Lines starting with '#' are treated as comments
108    ///
109    /// # Arguments
110    /// - `algorithm` - The hash algorithm to use for calculating hashes
111    /// - `path` - The path to the lookup file containing field names
112    ///
113    /// # Types
114    /// - `P` - A type that can be converted to a `Path` reference, such as `&str` or `String`
115    ///
116    /// # Errors
117    /// - If the file cannot be opened, a `JMapError::LookupFileNotFound` error is returned with the file path
118    ///
119    /// # Returns
120    /// A `Result` containing the new `FileHashTable` instance if successful, or a `JMapError` if the file cannot be read
121    pub fn from_file<P: AsRef<Path>>(algorithm: HashAlgorithm, path: P) -> Result<Self> {
122        let path = path.as_ref();
123        let file = File::open(path).map_err(|_| {
124            JMapError::LookupFileNotFound(path.display().to_string())
125        })?;
126
127        let reader = BufReader::new(file);
128        let mut lookup = HashMap::new();
129
130        for line in reader.lines() {
131            let line = line?;
132            let line = line.trim();
133
134            // Skip comments and empty lines
135            if line.is_empty() || line.starts_with('#') {
136                continue;
137            }
138
139            let hash = algorithm.calc(line);
140            lookup.insert(hash, line.to_string());
141        }
142
143        Ok(Self { algorithm, lookup })
144    }
145
146    /// Get the hash algorithm used by this table
147    ///
148    /// # Returns
149    /// The `HashAlgorithm` instance representing the hash algorithm used by this `FileHashTable`
150    pub fn algorithm(&self) -> HashAlgorithm {
151        self.algorithm
152    }
153}
154
155/// Implementation of the `HashTable` trait for `FileHashTable`
156/// This allows `FileHashTable` to be used wherever a `HashTable` is expected, providing methods to calculate hashes, find field names by hash, and add new field names to the lookup table
157impl HashTable for FileHashTable {
158    fn calc(&self, field_name: &str) -> u32 {
159        self.algorithm.calc(field_name)
160    }
161
162    fn find(&self, hash: u32) -> String {
163        self.lookup
164            .get(&hash)
165            .cloned()
166            .unwrap_or_else(|| format!("[{:08X}]", hash))
167    }
168
169    fn add(&mut self, field_name: &str) -> u32 {
170        let hash = self.calc(field_name);
171        self.lookup.entry(hash).or_insert_with(|| field_name.to_string());
172        hash
173    }
174}
175
176/// Create a hash table configured for Super Mario Galaxy 1/2
177///
178/// This uses the JGadget hash algorithm and loads the lookup file
179/// from the default location if available
180pub fn smg_hash_table() -> FileHashTable {
181    FileHashTable::new(HashAlgorithm::SMG)
182}
183
184/// Create a hash table for Super Mario Galaxy with a custom lookup file
185pub fn smg_hash_table_with_lookup<P: AsRef<Path>>(path: P) -> Result<FileHashTable> {
186    FileHashTable::from_file(HashAlgorithm::SMG, path)
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn test_hash() {
195        /// Known hash values from Super Mario Galaxy (verified with this [hash calculator](https://mariogalaxy.org/hash))
196        assert_eq!(calc_hash("ScenarioNo"), 0xED08B591);
197        assert_eq!(calc_hash("ZoneName"), 0x3666C077);
198    }
199
200    #[test]
201    fn test_hash_table() {
202        let mut table = smg_hash_table();
203        
204        // Add a field and verify lookup
205        let hash = table.add("TestField");
206        assert_eq!(table.find(hash), "TestField");
207        
208        // Unknown hash should return hex representation
209        let unknown = table.find(0xDEADBEEF);
210        assert_eq!(unknown, "[DEADBEEF]");
211    }
212}