diff --git a/tools/cheat-builder/build.js b/tools/cheat-builder/build.js index 943cbc4..0bfe10c 100644 --- a/tools/cheat-builder/build.js +++ b/tools/cheat-builder/build.js @@ -116,7 +116,7 @@ const format = valid => { const val = serial.charCodeAt(3); for( const { id, bin } of cheat ){ const off = ret[2] + size.get(ret[1]); - ret[0] = concat(ret[0], [val | (off << 3), id]) + ret[0] = concat(ret[0], [val | (off << 3), id])// 为什么移位是3不是8,看看align ret[1] = concat(ret[1], bin); } } @@ -207,6 +207,7 @@ const start = async () => { const content = await format( roms ); await writeFile( "gba.acl", content ); + process.exit(0); } start() diff --git a/tools/cheat-builder/cheats.tar.bz2 b/tools/cheat-builder/cheats.tar.bz2 index d33e217..7c3fadb 100644 Binary files a/tools/cheat-builder/cheats.tar.bz2 and b/tools/cheat-builder/cheats.tar.bz2 differ diff --git a/tools/cheat-builder/cht.js b/tools/cheat-builder/cht.js index 741b73c..82b5e8d 100644 --- a/tools/cheat-builder/cht.js +++ b/tools/cheat-builder/cht.js @@ -66,15 +66,18 @@ const read = (cht, info, order) => { case "]": if (state == READ_LOCK) { state = WAIT_HOLE; - currlock.name = token_str(); - currlock.holes = []; - if (currlock.name.toLowerCase() == "gameinfo"){ + const name = token_str(); + if (name.toLowerCase() == "gameinfo"){ retval.push( lade(locks, info, order) ); state = WAIT_PART; token = []; locks = []; currlock = null; } + else { + currlock.name = name; + currlock.holes = []; + } } else if(state == WAIT_PART) {} else if (state == NEED_PART) state = WAIT_PART; @@ -393,11 +396,11 @@ const lade = (list, info, order) => { enttable.push(makeEntry(makeID(0, 0), rootstr)); enttable.sort((a, b) => a.id - b.id); - names.sort( (a,b) => b.length - a.length ); + names.sort( (a,b) => a.length == b.length ? a.localeCompare(b) : b.length - a.length ); let strtable = new Uint8Array(1024); // 保存字符串常量 let idxtable = new Uint16Array(128); // 从字符串常量索引出来的字符串列表 - let cmdtable = new Uint32Array(4096); // 保存选项对应指令 + let cmdtable = new Uint32Array(2048); // 保存选项对应指令 const strOffsetCache = {}; const size = {"str": 0, "cmd": 0, "idx": 0}; diff --git a/tools/cheat-builder/make-acl.rs b/tools/cheat-builder/make-acl.rs new file mode 100644 index 0000000..1c08590 --- /dev/null +++ b/tools/cheat-builder/make-acl.rs @@ -0,0 +1,719 @@ +use std::fs; +use serde_json::Value; +use byteorder::{LittleEndian, ByteOrder}; +use std::collections::HashMap; +use std::collections::BTreeMap; + +type Module = Vec; +type LockKey = Vec; +type LockHole = (String, Vec< LockKey >); + +#[derive(Clone)] +#[derive(Default)] +struct Lock { + name: String, + holes: Vec< LockHole >, +} + +#[derive(Debug)] +enum ParserState { + WaitLock, + ReadLock, + WaitHole, + ReadHole, + ReadKey, + WaitPart, + NeedPart, +} +const PASSHOLE: [&str; 2] = ["text", "off"]; +const HEX: &str = "0123456789abcdefABCDEF"; + +struct ParserCtx<'a>{ + state: ParserState, + token: Vec, + line: u32, + + file: &'a String, + code: u32, + + locks: Vec< Lock >, + currlock: Option, + currhole: Option, + + output: &'a mut Vec< (u32, Module) >, +} + +fn token_str( ctx: &mut ParserCtx ) -> String { + if ctx.token.len() == 0 { + "".to_string() + } + else { + let retval = String::from_iter( &ctx.token ); + ctx.token.clear(); + retval + } +} + +fn error( ctx: &ParserCtx, str: String ) { + if ctx.currlock.is_some() { + panic!("{} at line: {} in lock {:?} [{}]", str, ctx.line, ctx.currlock.iter().next().unwrap().name, ctx.file); + } + else { + panic!("{} at line: {} in lock {:?} [{}]", str, ctx.line, "".to_string(), ctx.file); + } +} + +enum EntData<'a> { + Names(Vec<&'a String>), + Cmds(&'a Vec), +} + +struct LadeCtx { + strtable: Vec, + idxtable: Vec, + cmdtable: Vec, + strcache: HashMap, +} + +#[inline(always)] +fn make_id( a: usize, b: usize ) -> u32 { (a | b<<16).try_into().unwrap() } + +fn find_index( t: &Vec, tr: &[u8] ) -> i16 { + let rlen = tr.len(); + if rlen > t.len() { return -1; } + + let limit = t.len() - rlen; + for i in 0..limit { + if t[i + rlen] != 0 { + continue + } + + let mut eq = true; + for j in 0..rlen { + if t[i+j] != tr[j] { + eq = false; + break; + } + } + if eq { return i.try_into().unwrap(); } + } + -1 +} + +fn ut_push( t: &mut Vec, tr: &[u8] ) -> u16 { + let off = t.len(); + t.reserve( tr.len() + 1 ); + t.extend_from_slice(tr); + t.push(0); + off.try_into().unwrap() +} + +fn add_string( list: &Vec<&String>, ctx: &mut LadeCtx ) -> Vec { + let mut ret: Vec = Vec::with_capacity(list.len()); + for tr in list.iter() { + let res = ctx.strcache.get(&tr.to_string()); + if let Some(pos) = res { + ret.push( *pos ); + } + else { + let code: &[u8] = tr.as_bytes(); + let off: i16 = find_index(&ctx.strtable, code); + let res: u16 = if off < 0 { ut_push(&mut ctx.strtable, code) } else { off.try_into().unwrap() }; + ctx.strcache.insert(tr.to_string(), res); + ret.push( res ); + } + } + return ret; +} + +fn add_str_index( list: &Vec, ctx: &mut LadeCtx ) -> u32 { + if list.len() == 0 { return 0; } + + if ctx.strtable.len() > 0xffff { + panic!("string table is too big.") + } + + let ret: u32 = ctx.idxtable.len().try_into().unwrap(); + ctx.idxtable.reserve( list.len() ); + ctx.idxtable.extend( list.iter() ); + + return ret*2; +} + +fn assemble_cheat( list: &Vec, dup: &mut BTreeMap, hole: u16, order: &String ) -> Vec { + let from_t_addr = |taddr| { + match u32::from_str_radix(taddr, 16) { + Ok(n) => { + if n == 0 || n > 0x41fffff { + panic!("get an invalid address: {}", taddr); + } + + if n < 0x3ffff { return n | 0x200_0000; } + else if 0 == (n & 0xf000000) { return (n&0xffff) | 0x3000000; } + else { return n; } + }, + Err(_e) => panic!("get an invalid address: {} in {}", taddr, order), + } + }; + + let mut addrval: BTreeMap = BTreeMap::new(); + for command in list.iter() { + if command.len() == 0 { continue; } + + let mut iter = command.iter(); + let addr = from_t_addr( iter.next().unwrap() ); + let cnt = (command.len() - 1).try_into().unwrap(); + for pos in 0..cnt { + let r = iter.next().unwrap(); + match u32::from_str_radix(r, 16) { + Ok(val) => { addrval.insert(addr + pos, val); }, + Err(_e) => panic!("cannot get valid cheat val {} in {}", r, order), + } + } + } + + let mut ordered: Vec<(u32, u32)> = addrval.iter().map(|(&a,&b)| (a,b)).collect(); + ordered.sort_by( |a, b| a.0.cmp(&b.0) ); + let mut blocks: Vec = Vec::new(); + let mut curr: (u32, u32, u32) = (0, 0, 0); + for (addr, value) in ordered.iter() { + dup.insert(*addr, hole); + + if curr.2 == 0 { + curr = (*addr, *value, 1); + } + else { + if curr.1 == *value && curr.0 + curr.2 == *addr { + curr.2 = curr.2 + 1; + } + else { + blocks.push( curr.0 ); + blocks.push( (curr.2<<8) | curr.1 ); + curr = (*addr, *value, 1); + } + } + } + if curr.2 != 0 { + blocks.push( curr.0 ); + blocks.push( (curr.2<<8) | curr.1 ); + } + return blocks; +} + +fn add_command( list: &Vec, ctx: &mut LadeCtx ) -> u32 { + let ret:u32 = ctx.cmdtable.len().try_into().unwrap(); + ctx.cmdtable.reserve( list.len() ); + ctx.cmdtable.extend( list.iter() ); + return ret * 4; +} + +fn lade( list: &Vec, code: u32, order: &String ) -> (u32, Module) { + let mut rootstr: Vec<&String> = Vec::new(); + let mut names: Vec<&String> = Vec::new(); + let mut enttable: Vec<(u32, EntData)> = Vec::new(); + + for (i, lock) in list.iter().enumerate() { + rootstr.push( &lock.name ); + names.push( &lock.name ); + + let mut holestr: Vec<&String> = Vec::with_capacity( lock.holes.len() ); + let colname = lock.holes.len() > 1; + for (index, (name, cmd)) in lock.holes.iter().enumerate() { + holestr.push( name ); + if colname { names.push( name ); } + enttable.push( (make_id(i+1, index+1), EntData::Cmds(&cmd)) ); + } + + enttable.push( (make_id(i+1, 0), EntData::Names(holestr)) ); + } + enttable.push( (make_id(0, 0), EntData::Names(rootstr) ) ); + + if enttable.len() > 0xffff { + panic!("entry count is overflow in file {}", order); + } + + enttable.sort_by( |a, b| a.0.cmp( &b.0 ) ); + names.sort_by( |a, b| if a.len() == b.len() { a.cmp(&b) } else { b.len().cmp( &a.len() ) } ); + + let mut ctx: LadeCtx = LadeCtx{ + strtable: Vec::with_capacity( 1024 ), + idxtable: Vec::with_capacity( 128 ), + cmdtable: Vec::with_capacity( 2048 ), + strcache: HashMap::new(), + }; + add_string(&names, &mut ctx); + + let mut entbytelist: Vec<(u32, u32, u32)> = Vec::with_capacity( enttable.len() ); + let emptylist: Vec<&String> = Vec::new(); + let mut holedup: BTreeMap = BTreeMap::new(); + for (id, data) in enttable.iter() { + match data { + EntData::Names( names ) => { + let idxlist = add_string( if *id == 0 { names } else { if names.len() > 1 { names } else { &emptylist } }, &mut ctx ); + let location = add_str_index( &idxlist, &mut ctx ); + entbytelist.push( (*id, location, idxlist.len().try_into().unwrap()) ); + }, + EntData::Cmds( cmds ) => { + let armbytecode = assemble_cheat( cmds, &mut holedup, (id & 0xffff).try_into().unwrap(), &order ); + let location = add_command( &armbytecode, &mut ctx ); + entbytelist.push( (*id, location, armbytecode.len().try_into().unwrap()) ); + }, + } + } + + let mut xv = code; + for cmd in ctx.cmdtable.iter() { + xv = xv ^ cmd; + } + + let bin = pack( entbytelist, &ctx.strtable, &ctx.idxtable, &ctx.cmdtable ); + return (xv, bin); +} + +fn align( n: usize, base: usize ) -> usize { + let ext = n % base; + if ext == 0 { n } else { n + base - ext } +} + +fn pack( entlist: Vec<(u32,u32,u32)>, strlist: &Vec, idxlist: &Vec, cmdlist: &Vec ) -> Module { + let entrysize: usize = 12; + let strbase = 2 + entlist.len() * entrysize; + let idxbase = strbase + strlist.len(); + let cmdbase = idxbase + idxlist.len() * 2;// u16=2B + let nonalign_size = cmdbase + cmdlist.len() * 4;// u32=4B + let size = align(nonalign_size, 32); + + let mut result: Module = vec![0u8; size]; + LittleEndian::write_u16(&mut result[0..2], entlist.len().try_into().unwrap()); + + for (i, entry) in entlist.iter().enumerate() { + let offset = 2 + i * entrysize; + let locbase:u32; + if entry.0 > 0xffff { + locbase = cmdbase.try_into().unwrap(); + } + else { + locbase = idxbase.try_into().unwrap(); + } + LittleEndian::write_u32(&mut result[offset..offset+4], entry.0); + LittleEndian::write_u32(&mut result[offset+4..offset+8], locbase + entry.1); + LittleEndian::write_u32(&mut result[offset+8..offset+12], entry.2); + } + + // string table + result[strbase..idxbase].copy_from_slice( &strlist ); + + // idxes table + LittleEndian::write_u16_into(idxlist, &mut result[idxbase..cmdbase]); + + // instruction table + LittleEndian::write_u32_into(cmdlist, &mut result[cmdbase..nonalign_size]); + + return result; +} + +fn incr( ch: char, ctx: &mut ParserCtx ) { + match ch { + '[' => { + match ctx.state { + ParserState::WaitLock | ParserState::ReadHole | ParserState::NeedPart => { + ctx.state = ParserState::ReadLock; + if ctx.currlock.is_some() { + let currlock = ctx.currlock.clone().unwrap(); + ctx.locks.push( currlock ); + } + let tmp = Lock { + name: "".to_string(), + holes: Vec::new(), + }; + ctx.currlock = Some( tmp ); + }, + ParserState::WaitPart => {}, + _ => error( ctx, format!("error occur [ on {:?}", ctx.state) ), + } + }, + ']' => { + match ctx.state { + ParserState::ReadLock => { + ctx.state = ParserState::WaitHole; + let name = token_str(ctx); + if name.to_lowercase() == "gameinfo" { + ctx.output.push( lade(&ctx.locks, ctx.code, &ctx.file) ); + ctx.state = ParserState::WaitPart; + ctx.locks = Vec::new(); + ctx.currhole = None; + ctx.currlock = None; + } + else if !name.is_empty() { + ctx.currlock = Some( Lock { + name: name.to_string(), + holes: Vec::new(), + }); + ctx.currhole = None; + } + else { + panic!("empty lock name {}", ctx.file); + } + }, + ParserState::WaitPart => {}, + ParserState::NeedPart => { + ctx.state = ParserState::WaitPart; + }, + _ => error( ctx, format!("error occur ] on {:?}", ctx.state) ), + } + }, + '\r' | '\n' => { + match ctx.state { + ParserState::WaitHole => { + ctx.state = ParserState::ReadHole; + }, + ParserState::ReadKey => { + let token = token_str(ctx); + let mut pass = false; + if let Some(hole) = &mut ctx.currhole { + let (name, ref mut keys) = hole; + if !token.is_empty() { + if let Some(key) = keys.last_mut() { + key.push( token ); + } + } + if PASSHOLE.iter().any( |&s| s == name.to_lowercase() ) { + pass = true; + } + } + if pass == false { + if let Some(lock) = &mut ctx.currlock { + lock.holes.push( ctx.currhole.take().unwrap() ); + } + } + + ctx.state = ParserState::ReadHole; + }, + ParserState::ReadLock => { + error(ctx, format!("error occur newline on {:?}", ctx.state)); + }, + ParserState::ReadHole => { + if ctx.token.len() > 0 && ctx.token.iter().any( |c| *c != ' ' && *c != '\t' ) { + error(ctx, format!("error occur newline on {:?}", ctx.state)); + } + else { + ctx.token.clear(); + } + }, + ParserState::WaitPart => { + ctx.state = ParserState::NeedPart; + }, + _ => {}, + } + if ch == '\n' { + ctx.line += 1; + } + }, + '=' => { + match ctx.state { + ParserState::ReadHole => { + ctx.state = ParserState::ReadKey; + ctx.currhole = Some( (token_str(ctx), vec![Vec::new()]) ); + }, + ParserState::ReadLock => { + ctx.token.push( ch ); + }, + ParserState::WaitPart => {}, + ParserState::NeedPart => { + ctx.state = ParserState::WaitPart; + }, + _ => error( ctx, format!("error occur = on {:?}", ctx.state) ), + } + }, + ',' => { + match ctx.state { + ParserState::ReadKey => { + if ctx.token.len() > 0 { + let token = token_str(ctx); + ctx.currhole.as_mut().map( + |hole| { + let (_name, ref mut keys) = hole; + if !token.is_empty() { + if let Some(key) = keys.last_mut() { + key.push( token ); + } + } + true + } + ); + } + }, + ParserState::ReadHole => { + // 秘籍数据需要换行才能写完的情况 + let token = token_str(ctx); + ctx.currlock.as_mut().map( + |lock| { + ctx.currhole = lock.holes.pop().map( + |hole| { + let (name, mut keys) = hole; + if !token.is_empty() { + if let Some(key) = keys.last_mut() { + key.push( token ); + } + } + (name, keys) + } + ); + } + ); + ctx.state = ParserState::ReadKey; + } + ParserState::ReadLock => { + ctx.token.push( ch ); + }, + ParserState::WaitPart => {}, + ParserState::NeedPart => { + ctx.state = ParserState::WaitPart; + }, + _ => error( ctx, format!("error occur , on {:?}", ctx.state) ), + } + }, + ';' => { + match ctx.state { + ParserState::ReadKey => { + let token = token_str(ctx); + ctx.currhole.as_mut().map( + |hole| { + let (_name, ref mut keys) = hole; + if !token.is_empty() { + if let Some(key) = keys.last_mut() { + key.push( token ); + } + } + keys.push( Vec::new() ); + true + } + ); + }, + ParserState::ReadLock => { + ctx.token.push( ch ); + }, + ParserState::ReadHole => { + let token = token_str(ctx); + ctx.currlock.as_mut().map( + |lock| { + ctx.currhole = lock.holes.pop().map( + |hole| { + let (name, mut keys) = hole; + if !token.is_empty() { + if let Some(key) = keys.last_mut() { + key.push( token ); + } + } + keys.push( Vec::new() ); + (name, keys) + } + ); + } + ); + ctx.state = ParserState::ReadKey; + }, + ParserState::WaitPart => {}, + ParserState::NeedPart => { + ctx.state = ParserState::WaitPart; + }, + _ => error(ctx, format!("error occur ; on {:?}", ctx.state)), + } + }, + ' ' => { + match ctx.state { + ParserState::ReadLock | ParserState::ReadHole => { + ctx.token.push( ch ); + }, + ParserState::NeedPart => { + ctx.state = ParserState::WaitPart; + }, + _ => {}, + } + }, + _ => { + match ctx.state { + ParserState::ReadLock | ParserState::ReadHole => { + ctx.token.push( ch ); + }, + ParserState::ReadKey => { + if String::from(HEX).contains(ch) == true { + ctx.token.push( ch ); + } + else if let Some(hole) = &ctx.currhole { + let (name, _key) = hole; + if name == "text" { + ctx.token.push( ch ); + } + } + else { + error(ctx, format!("error occur {} on {:?}", ch, ctx.state)); + } + }, + ParserState::WaitPart => {}, + ParserState::NeedPart => { + ctx.state = ParserState::WaitPart; + }, + _ => error(ctx, format!("error occur {} on {:?}", ch, ctx.state)), + } + }, + } +} + +fn done(ctx: &mut ParserCtx) { + match ctx.state { + ParserState::WaitPart | ParserState::NeedPart => {}, + _ => error( ctx, format!("error occur eof on {:?}", ctx.state) ), + } +} + +fn parse( data: String, serial: &String, order: &String ) -> Vec< (u32, Module) > { + let mut ret: Vec<(u32, Module)> = Vec::new(); + let mut context = ParserCtx { + state: ParserState::WaitLock, + token: Vec::new(), + line: 1, + + file: order, + code: { + let mut r:u32 = 0; + for v in serial.as_bytes().iter() { + let u:u32 = *v as u32; + r = u | r << 8; + } + r + }, + + locks: Vec::new(), + currlock: None, + currhole: None, + + output: &mut ret, + }; + for ch in data.chars() { + incr( ch, &mut context ); + } + done( &mut context ); + return ret; +} + +fn loadlist() -> Vec<(String,String)> { + let mut retval: Vec<(String, String)> = Vec::new(); + + let file_content = fs::read_to_string("./serial.json") + .expect("Unable to read file"); + + let json_data: Value = serde_json::from_str(&file_content) + .expect("JSON was not well-formatted"); + + if let Value::Object(map) = json_data { + for (key, value) in map.iter() { + if let Value::String(serial) = value { + if serial != "????" { + retval.push( (key.to_string(), serial.to_string()) ); + } + } + } + } else { + panic!("serial.json with invalid format."); + } + return retval; +} + +fn transform<'a>( list: &'a Vec<(String, String)> ) -> Vec<(&'a String, Vec<(u32, Module)>)> { + let mut retval: Vec<(&String, Vec<(u32, Module)>)> = Vec::new(); + for (order, serial) in list.iter() { + let file = format!("./gba/{}.u8", order); + let chtdata = fs::read_to_string(file).unwrap_or("".to_string()); + if chtdata.len() > 0 { + let cheats = parse( chtdata.to_string(), serial, order ); + retval.push( (serial, cheats) ); + } + else { + println!("no data {}", order); + } + } + return retval; +} + +fn format<'a>( mut cheats: Vec<(&'a String, Vec<(u32, Module)>)> ) -> Vec { + cheats.sort_by( |a, b| a.0.cmp(&b.0) ); + println!("valid rom has {}", cheats.len()); + + let (sers, offs, chtc, maxl, _) = { + let mut ret: (Vec, Vec, usize, usize, String) = (vec![], vec![], 0, 0, "".to_string()); + let mut last: usize = 0; + for game in cheats.iter() { + let (serial, cheat) = game; + let val = serial.get(0..3).expect("not valid serial"); + if ret.4.ne(val) { + if ret.1.len() > 0 && ret.2 - last > ret.3 { + ret.3 = ret.2 - last; + } + ret.0.extend( val.as_bytes() ); + ret.1.push( ret.2.try_into().unwrap() ); + last = ret.2; + ret.4 = val.to_string(); + } + ret.2 = ret.2 + cheat.len(); + } + if ret.1.len() > 0 { + if ret.2 - last > ret.3 { + ret.3 = ret.2 - last; + } + ret.1.push( ret.2.try_into().unwrap() ); + } + ret + }; + let (cheats, expanded, _) = { + let mut ret: (Vec, Vec, usize) = (vec![], vec![], align(8 + sers.len() + offs.len()*2 + chtc * 8, 32)); + for game in cheats.iter() { + let (serial, cheat) = game; + let val = serial.chars().nth(3).expect("invalid serial"); + for (id, bin) in cheat.iter() { + let off: u32 = (ret.2 + ret.1.len()).try_into().unwrap(); + ret.0.push( (val as u32) | (off << 3) ); + ret.0.push( *id ); + ret.1.extend( bin ); + } + } + ret + }; + println!("name: {} cheats: {} maxl: {}", sers.len(), chtc, maxl); + + let serialbase = 8; + let offsetbase = serialbase + sers.len(); + let cheatbase = offsetbase + offs.len() * 2; + let expandbase = align( cheatbase + cheats.len() * 4, 32 ); + let total = expandbase + expanded.len(); + + let mut output: Vec = vec![0u8; total]; + output[0..4].copy_from_slice(&['A' as u8, 'C' as u8, 'L' as u8, 1]); + LittleEndian::write_u16(&mut output[4..6], (sers.len() / 3).try_into().unwrap()); + LittleEndian::write_u16(&mut output[6..8], maxl.try_into().unwrap()); + + output[serialbase..offsetbase].copy_from_slice( &sers ); + LittleEndian::write_u16_into(&offs, &mut output[offsetbase..cheatbase]); + LittleEndian::write_u32_into(&cheats, &mut output[cheatbase..cheatbase+cheats.len()*4]); + output[expandbase..expandbase+expanded.len()].copy_from_slice( &expanded ); + + return output; +} + +fn main() { + let list = loadlist(); + let roms = { + let ret = transform( &list ); + let mut idx: BTreeMap<&String, Vec<(u32, Module)>> = BTreeMap::new(); + for (serial, cheat) in ret.into_iter() { + if cheat.len() == 0 { + continue; + } + + idx.entry(serial).or_insert_with(Vec::new).extend( cheat ); + } + idx.into_iter().collect() + }; + let content = format( roms ); + let _ = fs::write("gba.acl", content); +}