Last active
April 15, 2021 16:05
-
-
Save ISSOtm/ff99c25d7af2ecf64469e49a77dad917 to your computer and use it in GitHub Desktop.
Invocation: `grep '^1' seeds.txt | ./seed_bruter`. Due to a BGB bug, you must copy it to the temp folder that this program creates...
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::convert::{TryFrom, TryInto}; | |
use std::env::{self, Args}; | |
use std::fs::{self, File}; | |
use std::io::{self, BufRead, BufReader, BufWriter, Read, Stdin, Write}; | |
use std::path::PathBuf; | |
use std::process::{self, Command, Stdio}; | |
use std::thread; | |
use std::time::Duration; | |
const NB_CHILDREN: usize = 8; | |
fn main() { | |
let mut args = env::args(); | |
let prog_name = args | |
.next() | |
.expect("What, the program doesn't have an argv[0]?"); | |
let bgb_path = require_arg(&mut args, &prog_name); | |
if bgb_path == "-h" || bgb_path == "--help" { | |
usage(&prog_name); | |
process::exit(0); | |
} | |
let save_state_path = require_arg(&mut args, &prog_name); | |
let seeds_path = require_arg(&mut args, &prog_name); | |
if args.next().is_some() { | |
usage(&prog_name); | |
process::exit(1); | |
} | |
// Create temp directory | |
let temp_dir_path = { | |
let mut base = env::temp_dir(); | |
base.push("seed_bruter"); | |
base | |
}; | |
fs::create_dir_all(&temp_dir_path).unwrap_or_else(|err| { | |
eprintln!( | |
"Error: Failed to create temp directory (\"{}\"): {}", | |
temp_dir_path.as_os_str().to_string_lossy(), | |
err | |
); | |
process::exit(2); | |
}); | |
// Grab input files | |
let mut save_state_file = require_input_file(&save_state_path, "save state"); | |
let mut seeds = BufReader::new(require_input_file(&seeds_path, "seeds")).lines(); | |
let mut save_state = Vec::new(); | |
save_state_file | |
.read_to_end(&mut save_state) | |
.unwrap_or_else(|err| { | |
eprintln!( | |
"Error: Failed to read save state file \"{}\": {}", | |
save_state_path, err | |
); | |
process::exit(2); | |
}); | |
// Get ready! | |
let mut children = Vec::with_capacity(NB_CHILDREN); | |
let mut nb_children = 0; | |
let mut more_seeds = true; | |
eprintln!("Go!"); | |
loop { | |
// Note: this point is only reached if at least one child can be spawned | |
// Don't attempt to spawn new children if all seeds have been exhaused | |
if more_seeds { | |
while nb_children < NB_CHILDREN { | |
// Attempt to spawn a new child | |
match seeds.next() { | |
Some(Ok(seed_str)) => { | |
// Read its seed | |
let seed = match u32::from_str_radix(&seed_str, 16) { | |
Ok(seed) => seed, | |
Err(err) => { | |
eprintln!("Error: Failed to parse seed \"{}\": {}", seed_str, err); | |
continue; | |
} | |
}; | |
let save_state_path = | |
match patch_save_state(seed, &temp_dir_path, &save_state) { | |
Ok(path) => path, | |
Err(err) => { | |
eprintln!( | |
"Error: Failed to create save state for seed \"{}\": {}", | |
seed_str, err | |
); | |
continue; | |
} | |
}; | |
let mut out_state_path = temp_dir_path.clone(); | |
out_state_path.push(format!("state.{:08x}.sn2", seed)); | |
let child = match Command::new(&bgb_path) | |
.stdout(Stdio::piped()) | |
.stderr(Stdio::piped()) | |
.stdin(Stdio::null()) | |
.arg("-hf") | |
.arg("-nobatt") | |
.arg("-rom") | |
.arg(&save_state_path) | |
.arg("-stateonexit") | |
.arg(out_state_path) | |
.arg("-br") | |
.arg("02:A59B") | |
.spawn() | |
{ | |
Ok(child) => child, | |
Err(err) => { | |
eprintln!( | |
"Error: Failed to spawn child for seed \"{}\": {}", | |
seed_str, err | |
); | |
continue; | |
} | |
}; | |
let new = Some((seed, child)); | |
if children.len() < NB_CHILDREN { | |
children.push(new); | |
} else { | |
// Find a free slot, and overwrite it | |
debug_assert!(children.iter().any(|opt| opt.is_none())); | |
for i in 0..children.len() { | |
if children[i].is_none() { | |
children[i] = new; | |
break; | |
} | |
} | |
} | |
nb_children += 1; | |
} | |
Some(Err(err)) => { | |
eprintln!("Error: Failed to read a seed: {}", err); | |
continue; | |
} | |
None => { | |
// No more seeds, stop trying to spawn | |
eprintln!("Finished processing all seeds"); | |
more_seeds = false; | |
break; | |
} | |
} | |
} | |
} else { | |
// Otherwise, check if any children are left | |
if nb_children == 0 { | |
// All done, exit this loop | |
break; | |
} | |
} | |
// Wait for children to finish | |
while nb_children == NB_CHILDREN || !more_seeds { | |
thread::sleep(Duration::from_millis(1)); | |
for i in 0..children.len() { | |
// Skip empty entries | |
let (seed, child) = match children[i].as_mut() { | |
Some(pair) => pair, | |
None => continue, | |
}; | |
match child.try_wait() { | |
Err(err) => eprintln!("Error: Wait for child failed: {}", err), | |
Ok(Some(status)) => { | |
if !status.success() { | |
eprintln!( | |
"Warning: child (seed {:08x}) exited with status {}", | |
seed, status | |
); | |
// TODO: write stdout and stderr to files | |
} | |
match reap_child(*seed, &temp_dir_path) { | |
Err(err) => { | |
eprintln!("Error: Reading output save state failed: {}", err) | |
} | |
Ok(success) => println!( | |
"{:08x}: {}", | |
seed, | |
if success { "SUCCESS!" } else { "rejected" } | |
), | |
}; | |
children[i] = None; | |
nb_children -= 1; | |
} | |
Ok(None) => (), // Child isn't finished yet, wait | |
} | |
} | |
if nb_children == 0 && !more_seeds { | |
eprintln!("Done."); | |
return; | |
} | |
} | |
} | |
} | |
const WRAM_BASE_ADDR: usize = 0xC000; | |
const SEED_ADDR: usize = 0xDAB8; | |
const SEED_OFS: usize = SEED_ADDR - WRAM_BASE_ADDR; | |
fn patch_save_state( | |
seed: u32, | |
temp_dir_path: &PathBuf, | |
base_save_state: &[u8], | |
) -> Result<PathBuf, io::Error> { | |
// Create a new save state file | |
let mut state_file_path = temp_dir_path.clone(); | |
state_file_path.push(format!("state.{:08x}.sn1", seed)); | |
let mut state_file = BufWriter::new(File::create(&state_file_path)?); | |
for block in BgbSaveState(base_save_state) { | |
let mut block = block?; | |
// If block is the WRAM block, patch in the seed | |
if block.name == "WRAM" { | |
block | |
.data | |
.splice(SEED_OFS..SEED_OFS + 4, seed.to_be_bytes().iter().cloned()); | |
} | |
state_file.write(block.name.as_bytes())?; | |
state_file.write(&[0])?; | |
state_file.write(&u32::try_from(block.data.len()).unwrap().to_le_bytes())?; | |
state_file.write(&block.data)?; | |
} | |
Ok(state_file_path) | |
} | |
const VRAM_BASE_ADDR: usize = 0x8000; | |
const SCRN0_ADDR: usize = 0x9800; | |
const SCRN_VX_B: usize = 32; | |
const EXPECTED: [[u8; 20]; 18] = [ | |
[ | |
0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x2C, 0x2C, 0x2C, | |
0x2C, 0x2C, 0x2C, 0x2C, 0x2C, | |
], | |
[ | |
0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x2C, 0x03, 0x2C, | |
0x03, 0x2C, 0x2C, 0x2C, 0x2C, | |
], | |
[ | |
0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x03, 0x2C, 0x03, | |
0x2C, 0x2C, 0x2C, 0x2C, 0x2C, | |
], | |
[ | |
0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x2C, 0x2C, 0x2C, | |
0x2C, 0x2C, 0x2C, 0x2C, 0x2C, | |
], | |
[ | |
0x40, 0x41, 0x40, 0x41, 0x52, 0x52, 0x52, 0x52, 0x40, 0x41, 0x40, 0x41, 0x2C, 0x2C, 0x2C, | |
0x2C, 0x2C, 0x2C, 0x2C, 0x2C, | |
], | |
[ | |
0x50, 0x51, 0x50, 0x51, 0x52, 0x52, 0x52, 0x52, 0x50, 0x51, 0x50, 0x51, 0x2C, 0x2C, 0x2C, | |
0x2C, 0x2C, 0x2C, 0x2C, 0x2C, | |
], | |
[ | |
0x40, 0x41, 0x40, 0x41, 0x52, 0x52, 0x52, 0x52, 0x40, 0x41, 0x40, 0x41, 0x2C, 0x2C, 0x2C, | |
0x2C, 0x2C, 0x2C, 0x2C, 0x2C, | |
], | |
[ | |
0x50, 0x51, 0x50, 0x51, 0x52, 0x52, 0x52, 0x52, 0x50, 0x51, 0x50, 0x51, 0x2C, 0x2C, 0x2C, | |
0x2C, 0x2C, 0x2C, 0x2C, 0x2C, | |
], | |
[ | |
0x40, 0x41, 0x40, 0x41, 0x40, 0x41, 0x40, 0x41, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, | |
0x2C, 0x52, 0x52, 0x52, 0x52, | |
], | |
[ | |
0x50, 0x51, 0x50, 0x51, 0x50, 0x51, 0x50, 0x51, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, | |
0x2C, 0x52, 0x52, 0x52, 0x52, | |
], | |
[ | |
0x40, 0x41, 0x40, 0x41, 0x40, 0x41, 0x40, 0x41, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, | |
0x2C, 0x52, 0x52, 0x52, 0x52, | |
], | |
[ | |
0x50, 0x51, 0x50, 0x51, 0x50, 0x51, 0x50, 0x51, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, | |
0x2C, 0x52, 0x52, 0x52, 0x52, | |
], | |
[ | |
0x40, 0x41, 0x40, 0x41, 0x52, 0x52, 0x52, 0x52, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, | |
0x2C, 0x2C, 0x2C, 0x2C, 0x2C, | |
], | |
[ | |
0x50, 0x51, 0x50, 0x51, 0x52, 0x52, 0x52, 0x52, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, | |
0x2C, 0x2C, 0x2C, 0x2C, 0x2C, | |
], | |
[ | |
0x40, 0x41, 0x40, 0x41, 0x52, 0x52, 0x52, 0x52, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, | |
0x2C, 0x2C, 0x2C, 0x2C, 0x2C, | |
], | |
[ | |
0x50, 0x51, 0x50, 0x51, 0x52, 0x52, 0x52, 0x52, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, | |
0x2C, 0x2C, 0x2C, 0x2C, 0x2C, | |
], | |
[ | |
0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, | |
0x2C, 0x2C, 0x2C, 0x2C, 0x2C, | |
], | |
[ | |
0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x52, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, 0x2C, | |
0x2C, 0x2C, 0x03, 0x2C, 0x03, | |
], | |
]; | |
fn reap_child(seed: u32, temp_dir_path: &PathBuf) -> Result<bool, io::Error> { | |
let mut out_state_path = temp_dir_path.clone(); | |
out_state_path.push(format!("state.{:08x}.sn2", seed)); | |
for block in BgbSaveState(BufReader::new(File::open(out_state_path)?)) { | |
let block = block?; | |
if block.name == "PC" { | |
let pc = u16::from(block.data[0]) + u16::from(block.data[1]) * 256; | |
if pc != 0xA59B { | |
eprintln!( | |
"Warning: seed {:08x} stopped at ${:04x}, not $a59b", | |
seed, pc | |
); | |
} | |
} | |
if block.name == "VRAM" { | |
for (i, row) in EXPECTED.iter().enumerate() { | |
for (j, tile) in row.iter().enumerate() { | |
if block.data[SCRN0_ADDR - VRAM_BASE_ADDR + i * SCRN_VX_B + j] != *tile { | |
return Ok(false); | |
} | |
} | |
} | |
return Ok(true); | |
} | |
} | |
Err(io::Error::new( | |
io::ErrorKind::UnexpectedEof, | |
"Could not find VRAM block", | |
)) | |
} | |
// === Arg handling === | |
fn require_arg(args: &mut Args, prog_name: &str) -> String { | |
args.next().unwrap_or_else(|| { | |
usage(prog_name); | |
process::exit(1); | |
}) | |
} | |
fn usage(prog_name: &str) { | |
eprintln!( | |
"Usage: {} <bgb command> <save state path> <seeds path> | |
Note: \"-\" for a file name will use stdin; using it several times at once will result in undefined behavior", | |
prog_name | |
); | |
} | |
fn require_input_file(path: &str, name: &str) -> Input { | |
input_file(path).unwrap_or_else(|err| { | |
eprintln!("Error: Failed to open {} \"{}\": {}", name, path, err); | |
process::exit(2); | |
}) | |
} | |
// === Input file handling === | |
#[derive(Debug)] | |
enum Input { | |
Stdin(Stdin), | |
File(File), | |
} | |
fn input_file(path: &str) -> Result<Input, io::Error> { | |
if path == "-" { | |
Ok(Input::Stdin(io::stdin())) | |
} else { | |
File::open(path).map(|file| Input::File(file)) | |
} | |
} | |
impl Read for Input { | |
fn read(&mut self, buf: &mut [u8]) -> Result<usize, io::Error> { | |
use Input::*; | |
match self { | |
Stdin(stdin) => stdin.read(buf), | |
File(file) => file.read(buf), | |
} | |
} | |
} | |
// === BGB save state parsing === | |
struct BgbSaveState<R: Read>(R); | |
impl<R: Read> Iterator for BgbSaveState<R> { | |
type Item = Result<BgbSaveStateBlock, io::Error>; | |
fn next(&mut self) -> Option<Self::Item> { | |
// Read block name | |
let mut name_bytes = Vec::new(); | |
loop { | |
let mut buf = [0]; | |
match self.0.read(&mut buf) { | |
Ok(0) => { | |
// We have EOF! | |
if name_bytes.len() == 0 { | |
return None; | |
} else { | |
return Some(Err(io::Error::new( | |
io::ErrorKind::UnexpectedEof, | |
"Unterminated block name", | |
))); | |
} | |
} | |
Ok(_) => { | |
if buf[0] == 0 { | |
break; | |
} | |
name_bytes.push(buf[0]); | |
} | |
Err(err) => return Some(Err(err)), | |
} | |
} | |
let name = match String::from_utf8(name_bytes) { | |
Ok(name) => name, | |
Err(err) => return Some(Err(io::Error::new(io::ErrorKind::InvalidData, err))), | |
}; | |
// Read block size | |
let mut size_bytes = [0u8; 4]; | |
if let Err(err) = self.0.read_exact(&mut size_bytes) { | |
return Some(Err(err)); | |
} | |
let block_size = u32::from_le_bytes(size_bytes); | |
let mut data = vec![0; block_size.try_into().unwrap()]; | |
if let Err(err) = self.0.read_exact(&mut data) { | |
return Some(Err(err)); | |
} | |
Some(Ok(BgbSaveStateBlock { name, data })) | |
} | |
} | |
#[derive(Debug)] | |
struct BgbSaveStateBlock { | |
name: String, | |
data: Vec<u8>, | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment