Skip to content

Instantly share code, notes, and snippets.

@ISSOtm
Last active April 15, 2021 16:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ISSOtm/ff99c25d7af2ecf64469e49a77dad917 to your computer and use it in GitHub Desktop.
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...
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