起動するスクリプトIDを変更するLuaスクリプト
https://twitter.com/fujidig_game/status/1744746992033780007
memory.registerexec(0x02108da4, function () local ptr = memory.getregister("r13")+8 local scriptid = memory.readdword(ptr) if scriptid == 36111826 then -- キャラバンの古株さんのイベントだったら memory.writedword(ptr, 32021600) -- 合宿所のイベントに書き換え end end)
某ゲームのセーブデータの内容を読みやすく表示するプログラム
使い方
ROMのファイルシステムにあるcnvdat.binをカレントディレクトリにおいておく。 セーブデータをscrambled.binという名前でカレントディレクトリにおいておく。
実行すると次のような出力を得る。
use std::fs::File; use std::io::{self, Read}; use std::collections::HashMap; use std::cell::RefCell; use std::rc::Rc; use std::collections::HashSet; struct Command { typename: String, name: String, num_bits_in_file: usize, num_bits_in_game: usize, number: usize, unknown1: usize, unknown2: bool } type Struct = Vec<Member>; type StructMap = HashMap<String, Rc<RefCell<Struct>>>; #[derive(Debug)] struct Member { name : String, number: usize, tp : MemberType, } #[derive(Debug)] enum MemberType { Int(usize), Struct(String) } fn unscramble(binary : &mut Vec<u8>) { let mut random_bytes = [0; 3 * 4]; let mut random = [0; 3]; let mut random_pool: [u16; 256] = [0; 256]; //let size = binary.len() - 12; let size = 32640 - 12; let a = size / 16; let b = if a == 0 { 0xB5BDCF5A_u32 >> 0x10 } else { (a as u32) * (0xB5BDCF5A_u32 >> 0x10) >> 0x10 } as usize; let index = b + 0x20; let mut binary_index = index; let mut seed = 0xB5BDCF5A_u32; for i in 0..12 { seed = 0x5D588B65_u32.wrapping_mul(seed).wrapping_add(0x269EC3_u32); let value = if a == 0 { seed >> 16 } else { (seed >> 16) * (a as u32) >> 0x10 }; binary_index += (value as usize) + 2; random_bytes[i] = binary[binary_index]; binary[binary_index] = binary[size + i]; } for i in 0..3 { random[i] = u32::from_le_bytes(random_bytes[i * 4..i * 4 + 4].try_into().unwrap()); } for i in 0..256 { random[0] = 0x5D588B65_u32.wrapping_mul(random[0]).wrapping_add(0x269EC3); random_pool[i] = (random[0] >> 0x10) as u16; } for i in 0..size { let rem = (i + (size * (random_pool[i % 256] as usize)) / 0x10000) % size; if ((i < 4) || (19 < i)) && ((rem < 4) || (19 < rem)) { binary.swap(i, rem); } } for i in 0..size { random[1] = 0x5D588B65_u32.wrapping_mul(random[1]).wrapping_add(0x269EC3_u32); if i < 4 || 19 < i { binary[i] ^= (random[1] >> 24) as u8; } } for i in 0..size { random[2] = 0x5D588B65_u32.wrapping_mul(random[2]).wrapping_add(0x269EC3_u32); if i < 4 || 19 < i { let shift = ((random[2] >> 0x10) << 3) >> 0x10; binary[i] = (((binary[i] as u16) << 8 | (binary[i] as u16)) >> shift) as u8; } } } fn load_binary_file(path : &str) -> io::Result<Vec<u8>> { let mut file = File::open(path)?; let mut binary = Vec::new(); file.read_to_end(&mut binary)?; return Ok(binary); } fn read_u8(binary : & Vec<u8>, index : usize) -> u8 { binary[index] } fn read_u16(binary : & Vec<u8>, index : usize) -> u16 { u16::from_le_bytes(binary[index..index + 2].try_into().unwrap()) } fn read_cstr(binary : & Vec<u8>, index : usize) -> String { let maxlength = 256; let mut vec = vec![]; for i in index..index+maxlength { let byte = binary[i]; if byte == 0 { break; } vec.push(byte); } return String::from_utf8(vec).unwrap(); } fn make_structs(cnvdat : &Vec<u8>) -> StructMap { let max_commands = read_u16(&cnvdat, 8) as usize; let max_types = read_u16(&cnvdat, 10) as usize; let mut strings = vec![]; let mut commands = vec![]; for i in 0..max_types { let offset = read_u16(&cnvdat, 16 + max_commands * 8 + i * 2) as usize; let str = read_cstr(&cnvdat, 16 + max_commands * 8 + max_types * 2 + offset); strings.push(str); } for i in 0..max_commands { let value_0 = read_u16(&cnvdat, 16 + i * 8 + 0); let value_1 = read_u16(&cnvdat, 16 + i * 8 + 2); let value_2 = read_u16(&cnvdat, 16 + i * 8 + 4); let value_3 = read_u8(&cnvdat, 16 + i * 8 + 6) & 0x7f; let bit = read_u8(&cnvdat, 16 + i * 8 + 6) & 0x80 != 0; let value_4 = read_u8(&cnvdat, 16 + i * 8 + 7); let str1 = strings[(value_0 & 0x1fff) as usize].clone(); let str2 = strings[value_1 as usize].clone(); let command = Command { typename: str1, name: str2, num_bits_in_file: value_4 as usize, num_bits_in_game: value_3 as usize, number: value_2 as usize, unknown1: (value_0 >> 13) as usize, unknown2: bit, }; commands.push(command); } let mut map = StructMap::new(); let mut stack: Vec<Rc<RefCell<Vec<Member>>>> = vec![]; let mut counter_unnamed = 0; for command in &commands { if command.number == 0 { stack.pop(); } else if command.typename == "@" { let member = Member { name: command.name.clone(), number: command.number, tp: MemberType::Int(command.num_bits_in_file) }; if command.unknown1 & 1 == 0 { stack.last().unwrap().try_borrow_mut().unwrap().push(member); } } else if command.num_bits_in_game == 0 { if command.typename == "*" { let struct_name = format!("unnamed{}", counter_unnamed); counter_unnamed += 1; let member = Member { name: command.name.clone(), number: command.number, tp: MemberType::Struct(struct_name.clone()) }; stack.last().unwrap().try_borrow_mut().unwrap().push(member); let strct = Rc::new(RefCell::new(vec![])); map.insert(struct_name.clone(), strct.clone()); stack.push(strct.clone()); } else if command.typename == "" { stack.pop(); } else { let member = Member { name: command.name.clone(), number: command.number, tp: MemberType::Struct(command.typename.clone()) }; stack.last().unwrap().try_borrow_mut().unwrap().push(member); } } else { if stack.len() > 0 { stack.pop(); } let strct = Rc::new(RefCell::new(vec![])); map.insert(command.typename.clone(), strct.clone()); stack.push(strct.clone()); } } //dbg!(&map); return map; } fn show_paths(bin: &Vec<u8>, map: &StructMap, path: &str, offset: usize, name: &str) -> usize { let str_members : HashSet<String> = HashSet::from([ "magicnumber".to_string(), "username".to_string(), "teamname".to_string(), "userName".to_string(), "mapname".to_string(), "mapfile".to_string(), "caption".to_string(), "string".to_string(), "mapinfoFile".to_string(), "effectname".to_string(), "name".to_string(), "nameWC".to_string(), "message".to_string(), ]); let members = map.get(name).unwrap().borrow(); let mut off = 0; for member in members.iter() { match &member.tp { &MemberType::Int(num_bits) => { let values = read_values(bin, offset + off, num_bits, member.number); if str_members.contains(&member.name) { let bytes: Vec<u8> = values.into_iter().map(|v| v as u8).take_while(|&v| v != 0).collect::<Vec<u8>>(); let (str, _, _) = encoding_rs::SHIFT_JIS.decode(&bytes); println!("{}.{} = \"{}\"", path, member.name, str); } else { print!("{}.{} = [", path, member.name); for i in 0..values.len() { if i > 0 { print!(", "); } print!("{}", values[i]); } println!("]"); } off += member.number * num_bits; } MemberType::Struct(nm) => { if member.number == 1 { off += show_paths(&bin, &map, &format!("{}.{}", path, member.name), offset + off, &nm); } else { for i in 0..member.number { off += show_paths(&bin, &map, &format!("{}.{}[{}]", path, member.name, i), offset + off, &nm); } } } } } return off; } fn read_values(bin : &Vec<u8>, index: usize, num_bits: usize, number: usize) -> Vec<usize> { let mut values = vec![]; for i in 0..number { let value = read_bits(bin, index + i * num_bits, num_bits); values.push(value); } return values; } fn read_bits(bin : &Vec<u8>, index: usize, num_bits: usize) -> usize { let mut res = 0; for i in 0..num_bits { res |= read_bit(bin, index + i) << i; } res } fn read_bit(bin : &Vec<u8>, index: usize) -> usize { ((bin[index / 8] >> (index % 8)) & 1) as usize } fn show_all(binary: &Vec<u8>, cnvdat: &Vec<u8>) { let map = make_structs(&cnvdat); show_paths(&binary, &map, "", 0, "st_all"); } fn main() -> io::Result<()> { let cnvdat = load_binary_file("cnvdat.bin")?; let mut binary = load_binary_file("scrambled.bin")?; unscramble(&mut binary); show_all(&binary, &cnvdat); Ok(()) }
某ゲームにおけるNGワード一覧を出力するLuaスクリプト
某ゲーム(ゲームコードだけ書いておくとNTR-BE8J-JPN)におけるNGワード一覧を出力するLuaスクリプトです。 DeSmuME (dev+ビルド)で動きます。
function read_cstr(addr) local chars = {} local i = 0 while true do local b = memory.readbyte(addr+i) if b == 0 then break end chars[i+1] = string.char(b) i = i + 1 end return table.concat(chars) end memory.registerexec(0x020318e4, function () print(read_cstr(memory.getregister("r1")):gsub("\n", "/")) end)
実行結果
https://twitter.com/fujidig_game/status/1742254404772995385
このデータはどこからきているかもう少し見てみると、"data/data_iz/pic2d/menu/MMName.SPF_"内に圧縮されて入っているようです。
某ゲームのセーブデータのスクランブルを解除するプログラム
このプログラムを使うとセーブデータのスクランブルを解除し、以下のようなファイルを得ることができます。
これに関連するTODOは以下の通り
- スクランブルをかける方のプログラムも書く
- チェックサムを直すプログラムも書く
- セーブデータの各領域が何を意味しているかはROMの中のあるファイルに従えばわかるので、それを使って人間に見やすく表示するプログラムを書く
以下はRustで書かれたプログラムです。
カレントディレクトリのファイルscrambled.bin (32640バイトのもの)を読み込み、スクランブルを解除し、ファイルunscrambled.binに出力します。
use std::fs::File; use std::io::{self, Read, Write}; fn unscramble(binary : &mut Vec<u8>) { let mut random_bytes = [0; 3 * 4]; let mut random = [0; 3]; let mut random_pool: [u16; 256] = [0; 256]; let size = binary.len() - 12; let a = size / 16; let b = if a == 0 { 0xB5BDCF5A_u32 >> 0x10 } else { (a as u32) * (0xB5BDCF5A_u32 >> 0x10) >> 0x10 } as usize; let index = b + 0x20; let mut binary_index = index; let mut seed = 0xB5BDCF5A_u32; for i in 0..12 { seed = 0x5D588B65_u32.wrapping_mul(seed).wrapping_add(0x269EC3_u32); let value = if a == 0 { seed >> 16 } else { (seed >> 16) * (a as u32) >> 0x10 }; binary_index += (value as usize) + 2; random_bytes[i] = binary[binary_index]; binary[binary_index] = binary[size + i]; } for i in 0..3 { random[i] = u32::from_le_bytes(random_bytes[i * 4..i * 4 + 4].try_into().unwrap()); } for i in 0..256 { random[0] = 0x5D588B65_u32.wrapping_mul(random[0]).wrapping_add(0x269EC3); random_pool[i] = (random[0] >> 0x10) as u16; } for i in 0..size { let rem = (i + (size * (random_pool[i % 256] as usize)) / 0x10000) % size; if ((i < 4) || (19 < i)) && ((rem < 4) || (19 < rem)) { binary.swap(i, rem); } } for i in 0..size { random[1] = 0x5D588B65_u32.wrapping_mul(random[1]).wrapping_add(0x269EC3_u32); if i < 4 || 19 < i { binary[i] ^= (random[1] >> 24) as u8; } } for i in 0..size { random[2] = 0x5D588B65_u32.wrapping_mul(random[2]).wrapping_add(0x269EC3_u32); if i < 4 || 19 < i { let shift = ((random[2] >> 0x10) << 3) >> 0x10; binary[i] = (((binary[i] as u16) << 8 | (binary[i] as u16)) >> shift) as u8; } } } fn main() -> io::Result<()> { let file_path: &str = "scrambled.bin"; let mut file = File::open(file_path)?; let mut binary = Vec::new(); file.read_to_end(&mut binary)?; unscramble(&mut binary); let file_path = "unscrambled.bin"; let mut file = File::create(file_path)?; file.write(&binary)?; Ok(()) }