2023-04-07 08:16:38 +10:00
|
|
|
use anyhow::{anyhow, bail, ensure, Result};
|
2023-04-07 08:59:28 +10:00
|
|
|
use clap::{arg, value_parser};
|
2023-04-07 08:16:38 +10:00
|
|
|
|
2023-04-07 06:09:44 +10:00
|
|
|
use std::{
|
2023-04-07 08:16:38 +10:00
|
|
|
fs,
|
2023-04-07 06:54:26 +10:00
|
|
|
io::{BufWriter, Write},
|
2023-04-07 06:09:44 +10:00
|
|
|
path::PathBuf,
|
|
|
|
};
|
|
|
|
|
2023-04-07 08:16:38 +10:00
|
|
|
fn main() -> Result<()> {
|
2023-04-07 08:59:28 +10:00
|
|
|
let matches = clap::Command::new("agb-gbafix")
|
|
|
|
.about("Convert elf files directly to a valid GBA ROM")
|
2023-04-07 08:43:57 +10:00
|
|
|
.arg(arg!(<INPUT> "Input elf file").value_parser(value_parser!(PathBuf)))
|
|
|
|
.arg(arg!(-o --output <OUTPUT> "Set output file, defaults to replacing INPUT's extension to .gba").value_parser(value_parser!(PathBuf)))
|
2023-04-07 09:03:00 +10:00
|
|
|
.arg(arg!(-t --title <TITLE> "Set the title. At most 12 bytes. Defaults to truncating the input file name"))
|
2023-04-07 08:56:19 +10:00
|
|
|
.arg(arg!(-c --gamecode <GAME_CODE> "Sets the game code, 4 bytes"))
|
|
|
|
.arg(arg!(-m --makercode <MAKER_CODE> "Set the maker code, 0-65535").value_parser(value_parser!(u16)))
|
|
|
|
.arg(arg!(-r --gameversion <VERSION> "Set the version of the game, 0-255").value_parser(value_parser!(u8)))
|
2023-04-07 08:43:57 +10:00
|
|
|
.get_matches();
|
2023-04-07 08:13:42 +10:00
|
|
|
|
2023-04-07 08:43:57 +10:00
|
|
|
let input = matches.get_one::<PathBuf>("INPUT").unwrap();
|
|
|
|
let output = match matches.get_one::<PathBuf>("output") {
|
|
|
|
Some(output) => output.clone(),
|
|
|
|
None => input.with_extension("gba"),
|
|
|
|
};
|
|
|
|
|
2023-04-07 08:56:19 +10:00
|
|
|
let mut header = gbafix::GBAHeader::default();
|
|
|
|
|
2023-04-07 09:03:00 +10:00
|
|
|
{
|
|
|
|
let title = if let Some(title) = matches.get_one::<String>("title") {
|
|
|
|
title.clone()
|
|
|
|
} else {
|
|
|
|
let title = input
|
|
|
|
.file_stem()
|
|
|
|
.ok_or_else(|| anyhow!("Invalid filename {}", input.to_string_lossy()))?
|
|
|
|
.to_string_lossy();
|
|
|
|
title.into_owned()
|
|
|
|
};
|
|
|
|
|
2023-04-07 08:56:19 +10:00
|
|
|
for (i, &c) in title.as_bytes().iter().enumerate().take(12) {
|
|
|
|
header.title[i] = c;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(maker_code) = matches.get_one::<u16>("makercode") {
|
|
|
|
header.maker_code = maker_code.to_le_bytes();
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(game_version) = matches.get_one::<u8>("gameversion") {
|
|
|
|
header.version = *game_version;
|
|
|
|
}
|
2023-04-07 08:43:57 +10:00
|
|
|
|
2023-04-07 08:56:19 +10:00
|
|
|
if let Some(game_code) = matches.get_one::<String>("gamecode") {
|
|
|
|
for (i, &c) in game_code.as_bytes().iter().enumerate().take(4) {
|
|
|
|
header.game_code[i] = c;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut output = BufWriter::new(fs::File::create(output)?);
|
2023-04-07 08:43:57 +10:00
|
|
|
let file_data = fs::read(input)?;
|
2023-04-07 06:09:44 +10:00
|
|
|
|
2023-04-07 08:56:19 +10:00
|
|
|
write_gba_file(file_data.as_slice(), header, &mut output)?;
|
2023-04-07 08:13:42 +10:00
|
|
|
|
|
|
|
output.flush()?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2023-04-07 08:56:19 +10:00
|
|
|
fn write_gba_file<W: Write>(
|
|
|
|
input: &[u8],
|
|
|
|
mut header: gbafix::GBAHeader,
|
|
|
|
output: &mut W,
|
|
|
|
) -> Result<()> {
|
2023-04-07 08:13:42 +10:00
|
|
|
let elf_file = elf::ElfBytes::<elf::endian::AnyEndian>::minimal_parse(input)?;
|
2023-04-07 06:09:44 +10:00
|
|
|
|
2023-04-07 06:54:26 +10:00
|
|
|
let section_headers = elf_file
|
|
|
|
.section_headers()
|
2023-04-07 08:16:38 +10:00
|
|
|
.ok_or_else(|| anyhow!("Failed to parse as elf file"))?;
|
2023-04-07 06:09:44 +10:00
|
|
|
|
2023-04-07 08:09:19 +10:00
|
|
|
const GBA_START_ADDRESS: u64 = 0x8000000;
|
|
|
|
let mut address = GBA_START_ADDRESS;
|
|
|
|
|
2023-04-07 06:54:26 +10:00
|
|
|
for section_header in section_headers.iter() {
|
2023-04-07 06:09:44 +10:00
|
|
|
const SHT_NOBITS: u32 = 8;
|
|
|
|
const SHT_NULL: u32 = 0;
|
|
|
|
const SHF_ALLOC: u64 = 2;
|
|
|
|
|
|
|
|
if (section_header.sh_type == SHT_NOBITS || section_header.sh_type == SHT_NULL)
|
|
|
|
|| section_header.sh_flags & SHF_ALLOC == 0
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2023-04-07 08:35:44 +10:00
|
|
|
if address < section_header.sh_addr {
|
|
|
|
for _ in address..section_header.sh_addr {
|
|
|
|
output.write_all(&[0])?;
|
|
|
|
}
|
|
|
|
|
|
|
|
address = section_header.sh_addr;
|
2023-04-07 08:09:19 +10:00
|
|
|
}
|
|
|
|
|
2023-04-07 06:54:26 +10:00
|
|
|
let (mut data, compression) = elf_file.section_data(§ion_header)?;
|
2023-04-07 06:09:44 +10:00
|
|
|
if let Some(compression) = compression {
|
2023-04-07 08:16:38 +10:00
|
|
|
bail!("Cannot decompress elf content, but got compression header {compression:?}");
|
2023-04-07 06:09:44 +10:00
|
|
|
}
|
|
|
|
|
2023-04-07 08:09:19 +10:00
|
|
|
if address == GBA_START_ADDRESS {
|
2023-04-07 06:55:11 +10:00
|
|
|
const GBA_HEADER_SIZE: usize = 192;
|
|
|
|
|
2023-04-07 08:16:38 +10:00
|
|
|
ensure!(
|
2023-04-07 06:55:11 +10:00
|
|
|
data.len() > GBA_HEADER_SIZE,
|
2023-04-07 06:54:26 +10:00
|
|
|
"first section must be at least as big as the gba header"
|
|
|
|
);
|
|
|
|
|
|
|
|
header.start_code = data[0..4].try_into().unwrap();
|
|
|
|
header.update_checksum();
|
|
|
|
|
|
|
|
let header_bytes = bytemuck::bytes_of(&header);
|
|
|
|
output.write_all(header_bytes)?;
|
|
|
|
|
2023-04-07 06:55:11 +10:00
|
|
|
data = &data[GBA_HEADER_SIZE..];
|
2023-04-07 08:09:19 +10:00
|
|
|
address += GBA_HEADER_SIZE as u64;
|
2023-04-07 06:54:26 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
output.write_all(data)?;
|
2023-04-07 08:09:19 +10:00
|
|
|
address += data.len() as u64;
|
2023-04-07 06:09:44 +10:00
|
|
|
}
|
|
|
|
|
2023-04-07 08:35:44 +10:00
|
|
|
let length = address - GBA_START_ADDRESS;
|
|
|
|
|
|
|
|
if !length.is_power_of_two() {
|
|
|
|
let required_padding = length.next_power_of_two() - length;
|
|
|
|
|
|
|
|
for _ in 0..required_padding {
|
|
|
|
output.write_all(&[0])?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-07 06:09:44 +10:00
|
|
|
Ok(())
|
|
|
|
}
|