Skip to main content

monsoon_cli/cli/
memory_init.rs

1//! Memory initialization utilities for CLI.
2//!
3//! This module handles parsing and applying memory initialization commands
4//! from CLI arguments or configuration files.
5//!
6//! # Supported Formats
7//!
8//! ## Command Line Format
9//! - Single value: `0x0050=0xFF`
10//! - Multiple values: `0x0050=0x01,0x02,0x03,0x04`
11//!
12//! ## File Formats (--init-file)
13//! - JSON
14//! - TOML
15//! - Binary
16
17use std::collections::HashMap;
18use std::io::Read;
19use std::path::Path;
20
21use monsoon_core::emulation::nes::Nes;
22
23/// Represents a memory initialization operation
24#[derive(Debug, Clone)]
25pub struct MemoryInit {
26    /// Starting address
27    pub address: u16,
28    /// Values to write starting at address
29    pub values: Vec<u8>,
30}
31
32impl MemoryInit {
33    /// Parse a memory init string in format `ADDR=VALUE` or `ADDR=V1,V2,...`
34    ///
35    /// # Examples
36    ///
37    /// ```
38    /// use monsoon_cli::cli::memory_init::MemoryInit;
39    ///
40    /// let init = MemoryInit::parse("0x0050=0xFF").unwrap();
41    /// assert_eq!(init.address, 0x0050);
42    /// assert_eq!(init.values, vec![0xFF]);
43    ///
44    /// let init = MemoryInit::parse("0x6000=0x01,0x02,0x03").unwrap();
45    /// assert_eq!(init.address, 0x6000);
46    /// assert_eq!(init.values, vec![0x01, 0x02, 0x03]);
47    /// ```
48    pub fn parse(s: &str) -> Result<Self, String> {
49        let (addr_str, values_str) = s.split_once('=').ok_or_else(|| {
50            format!(
51                "Invalid memory init format '{}'. Expected ADDR=VALUE or ADDR=V1,V2,...",
52                s
53            )
54        })?;
55
56        let address = parse_hex_u16(addr_str.trim())?;
57        let values = parse_values(values_str.trim())?;
58
59        if values.is_empty() {
60            return Err(format!("Memory init '{}' has no values", s));
61        }
62
63        Ok(Self {
64            address,
65            values,
66        })
67    }
68}
69
70/// Parse a hexadecimal u16 value
71fn parse_hex_u16(s: &str) -> Result<u16, String> {
72    let s = s
73        .strip_prefix("0x")
74        .or_else(|| s.strip_prefix("0X"))
75        .unwrap_or(s);
76    u16::from_str_radix(s, 16).map_err(|e| format!("Invalid hex address '{}': {}", s, e))
77}
78
79/// Parse a comma-separated list of hex u8 values
80fn parse_values(s: &str) -> Result<Vec<u8>, String> {
81    s.split(',')
82        .map(|v| {
83            let v = v.trim();
84            let v = v
85                .strip_prefix("0x")
86                .or_else(|| v.strip_prefix("0X"))
87                .unwrap_or(v);
88            u8::from_str_radix(v, 16).map_err(|e| format!("Invalid hex value '{}': {}", v, e))
89        })
90        .collect()
91}
92
93/// Memory initialization configuration loaded from a file
94#[derive(Debug, Clone, Default)]
95pub struct MemoryInitConfig {
96    /// CPU memory initializations
97    pub cpu: HashMap<u16, Vec<u8>>,
98    /// PPU memory initializations
99    pub ppu: HashMap<u16, Vec<u8>>,
100    /// OAM memory initializations
101    pub oam: HashMap<u16, Vec<u8>>,
102}
103
104impl MemoryInitConfig {
105    /// Load memory init configuration from a file.
106    ///
107    /// Supports JSON, TOML, and binary formats.
108    pub fn load_from_file(path: &Path) -> Result<Self, String> {
109        let extension = path
110            .extension()
111            .and_then(|e| e.to_str())
112            .unwrap_or("")
113            .to_lowercase();
114
115        match extension.as_str() {
116            "json" => Self::load_json(path),
117            "toml" => Self::load_toml(path),
118            "bin" | "binary" => Self::load_binary(path),
119            _ => Err(format!(
120                "Unsupported init file format '{}'. Use .json, .toml, or .bin",
121                extension
122            )),
123        }
124    }
125
126    /// Load from JSON file
127    fn load_json(path: &Path) -> Result<Self, String> {
128        let content = std::fs::read_to_string(path)
129            .map_err(|e| format!("Failed to read init file '{}': {}", path.display(), e))?;
130
131        let json: serde_json::Value = serde_json::from_str(&content)
132            .map_err(|e| format!("Failed to parse JSON init file: {}", e))?;
133
134        Self::from_json_value(&json)
135    }
136
137    /// Load from TOML file
138    fn load_toml(path: &Path) -> Result<Self, String> {
139        let content = std::fs::read_to_string(path)
140            .map_err(|e| format!("Failed to read init file '{}': {}", path.display(), e))?;
141
142        let toml: toml::Value = content
143            .parse()
144            .map_err(|e| format!("Failed to parse TOML init file: {}", e))?;
145
146        Self::from_toml_value(&toml)
147    }
148
149    /// Load from binary file (raw bytes for CPU memory starting at 0x0000)
150    fn load_binary(path: &Path) -> Result<Self, String> {
151        let mut file = std::fs::File::open(path).map_err(|e| {
152            format!(
153                "Failed to open binary init file '{}': {}",
154                path.display(),
155                e
156            )
157        })?;
158
159        let mut data = Vec::new();
160        file.read_to_end(&mut data)
161            .map_err(|e| format!("Failed to read binary init file: {}", e))?;
162
163        let mut config = Self::default();
164        config.cpu.insert(0x0000, data);
165        Ok(config)
166    }
167
168    /// Parse from JSON value
169    fn from_json_value(json: &serde_json::Value) -> Result<Self, String> {
170        let mut config = Self::default();
171
172        if let Some(cpu) = json.get("cpu").and_then(|v| v.as_object()) {
173            for (addr_str, values) in cpu {
174                let addr = parse_hex_u16(addr_str)?;
175                let vals = parse_json_array(values)?;
176                config.cpu.insert(addr, vals);
177            }
178        }
179
180        if let Some(ppu) = json.get("ppu").and_then(|v| v.as_object()) {
181            for (addr_str, values) in ppu {
182                let addr = parse_hex_u16(addr_str)?;
183                let vals = parse_json_array(values)?;
184                config.ppu.insert(addr, vals);
185            }
186        }
187
188        if let Some(oam) = json.get("oam").and_then(|v| v.as_object()) {
189            for (addr_str, values) in oam {
190                let addr = parse_hex_u16(addr_str)?;
191                let vals = parse_json_array(values)?;
192                config.oam.insert(addr, vals);
193            }
194        }
195
196        Ok(config)
197    }
198
199    /// Parse from TOML value
200    fn from_toml_value(toml: &toml::Value) -> Result<Self, String> {
201        let mut config = Self::default();
202
203        if let Some(cpu) = toml.get("cpu").and_then(|v| v.as_table()) {
204            for (addr_str, values) in cpu {
205                let addr = parse_hex_u16(addr_str)?;
206                let vals = parse_toml_array(values)?;
207                config.cpu.insert(addr, vals);
208            }
209        }
210
211        if let Some(ppu) = toml.get("ppu").and_then(|v| v.as_table()) {
212            for (addr_str, values) in ppu {
213                let addr = parse_hex_u16(addr_str)?;
214                let vals = parse_toml_array(values)?;
215                config.ppu.insert(addr, vals);
216            }
217        }
218
219        if let Some(oam) = toml.get("oam").and_then(|v| v.as_table()) {
220            for (addr_str, values) in oam {
221                let addr = parse_hex_u16(addr_str)?;
222                let vals = parse_toml_array(values)?;
223                config.oam.insert(addr, vals);
224            }
225        }
226
227        Ok(config)
228    }
229}
230
231/// Parse a JSON array of numbers to u8 values
232fn parse_json_array(value: &serde_json::Value) -> Result<Vec<u8>, String> {
233    match value {
234        serde_json::Value::Array(arr) => arr
235            .iter()
236            .map(|v| match v {
237                serde_json::Value::Number(n) => n
238                    .as_u64()
239                    .filter(|&n| n <= 255)
240                    .map(|n| n as u8)
241                    .ok_or_else(|| format!("Value {} out of range for u8", n)),
242                serde_json::Value::String(s) => {
243                    let s = s
244                        .strip_prefix("0x")
245                        .or_else(|| s.strip_prefix("0X"))
246                        .unwrap_or(s);
247                    u8::from_str_radix(s, 16)
248                        .map_err(|e| format!("Invalid hex value '{}': {}", s, e))
249                }
250                _ => Err("Expected number or hex string".to_string()),
251            })
252            .collect(),
253        serde_json::Value::Number(n) => {
254            let val = n
255                .as_u64()
256                .filter(|&n| n <= 255)
257                .map(|n| n as u8)
258                .ok_or_else(|| format!("Value {} out of range for u8", n))?;
259            Ok(vec![val])
260        }
261        _ => Err("Expected array or number".to_string()),
262    }
263}
264
265/// Parse a TOML array of numbers to u8 values
266fn parse_toml_array(value: &toml::Value) -> Result<Vec<u8>, String> {
267    match value {
268        toml::Value::Array(arr) => arr
269            .iter()
270            .map(|v| match v {
271                toml::Value::Integer(n) => {
272                    if *n >= 0 && *n <= 255 {
273                        Ok(*n as u8)
274                    } else {
275                        Err(format!("Value {} out of range for u8", n))
276                    }
277                }
278                toml::Value::String(s) => {
279                    let s = s
280                        .strip_prefix("0x")
281                        .or_else(|| s.strip_prefix("0X"))
282                        .unwrap_or(s);
283                    u8::from_str_radix(s, 16)
284                        .map_err(|e| format!("Invalid hex value '{}': {}", s, e))
285                }
286                _ => Err("Expected integer or hex string".to_string()),
287            })
288            .collect(),
289        toml::Value::Integer(n) => {
290            if *n >= 0 && *n <= 255 {
291                Ok(vec![*n as u8])
292            } else {
293                Err(format!("Value {} out of range for u8", n))
294            }
295        }
296        _ => Err("Expected array or integer".to_string()),
297    }
298}
299
300/// Apply memory initialization operations to the emulator
301pub fn apply_memory_init(
302    emu: &mut Nes,
303    cpu_inits: &[MemoryInit],
304    ppu_inits: &[MemoryInit],
305    oam_inits: &[MemoryInit],
306) {
307    // Apply CPU memory initializations
308    for init in cpu_inits {
309        for (offset, &value) in init.values.iter().enumerate() {
310            let addr = init.address.wrapping_add(offset as u16);
311            emu.cpu_mem_write(addr, value);
312        }
313    }
314
315    // Apply PPU memory initializations
316    for init in ppu_inits {
317        for (offset, &value) in init.values.iter().enumerate() {
318            let addr = init.address.wrapping_add(offset as u16);
319            emu.ppu_mem_write(addr, value);
320        }
321    }
322
323    // Apply OAM memory initializations
324    for init in oam_inits {
325        for (offset, &value) in init.values.iter().enumerate() {
326            // OAM is limited to 256 bytes (addresses 0x00-0xFF)
327            let addr = (init.address.wrapping_add(offset as u16)) & 0xFF;
328            emu.oam_write(addr, value);
329        }
330    }
331}
332
333/// Apply memory initialization from config file
334pub fn apply_memory_init_config(emu: &mut Nes, config: &MemoryInitConfig) {
335    // Apply CPU memory initializations
336    for (&addr, values) in &config.cpu {
337        for (offset, &value) in values.iter().enumerate() {
338            let target_addr = addr.wrapping_add(offset as u16);
339            emu.cpu_mem_write(target_addr, value);
340        }
341    }
342
343    // Apply PPU memory initializations
344    for (&addr, values) in &config.ppu {
345        for (offset, &value) in values.iter().enumerate() {
346            let target_addr = addr.wrapping_add(offset as u16);
347            emu.ppu_mem_write(target_addr, value);
348        }
349    }
350
351    // Apply OAM memory initializations
352    for (&addr, values) in &config.oam {
353        for (offset, &value) in values.iter().enumerate() {
354            let target_addr = (addr.wrapping_add(offset as u16)) & 0xFF;
355            emu.oam_write(target_addr, value);
356        }
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    #[test]
365    fn test_parse_single_value() {
366        let init = MemoryInit::parse("0x0050=0xFF").unwrap();
367        assert_eq!(init.address, 0x0050);
368        assert_eq!(init.values, vec![0xFF]);
369    }
370
371    #[test]
372    fn test_parse_multiple_values() {
373        let init = MemoryInit::parse("0x6000=0x01,0x02,0x03,0x04").unwrap();
374        assert_eq!(init.address, 0x6000);
375        assert_eq!(init.values, vec![0x01, 0x02, 0x03, 0x04]);
376    }
377
378    #[test]
379    fn test_parse_without_0x_prefix() {
380        let init = MemoryInit::parse("6000=FF").unwrap();
381        assert_eq!(init.address, 0x6000);
382        assert_eq!(init.values, vec![0xFF]);
383    }
384
385    #[test]
386    fn test_parse_with_spaces() {
387        let init = MemoryInit::parse("0x0050 = 0x01, 0x02, 0x03").unwrap();
388        assert_eq!(init.address, 0x0050);
389        assert_eq!(init.values, vec![0x01, 0x02, 0x03]);
390    }
391
392    #[test]
393    fn test_parse_invalid_format() {
394        assert!(MemoryInit::parse("0x0050").is_err());
395        assert!(MemoryInit::parse("invalid").is_err());
396    }
397
398    #[test]
399    fn test_parse_empty_values() {
400        assert!(MemoryInit::parse("0x0050=").is_err());
401    }
402}