package main; import ( "fmt" "os" "encoding/json" "encoding/binary" "strings" "bytes" "sort" "time" ) type Serial struct { order string serial string } type Packed struct { code uint32 data []byte } type Cheat struct { serial string cheats []Packed } type LockKey = []string type LockHole struct { name string keys []LockKey } type Lock struct { name string holes []LockHole } type EntData struct { id uint32 t uint strs [](*string) cmds *[]LockKey } const ( WaitLock = iota ReadLock WaitHole ReadHole ReadKey WaitPart NeedPart ) const ( EntNames = iota EntCmds ) func align(n uint32, basic uint32) uint32 { ext := n % basic if ext == 0 { return n } else { return n + basic - ext } } func find_index(t []byte, tr []byte) int { rlen, tlen := len(tr), len(t) limit := tlen - rlen for i := 0; i < limit; i++ { if t[i+rlen] != 0 { continue } eq := true for j := 0; j < rlen; j++ { if tr[j] != t[i+j] { eq = false break } } if eq { return i } } return -1 } func assemble_cheat(list *[]LockKey, dup map[uint32]uint16, hole uint16) []uint32 { var atoi = func(t string) uint32 { var n uint32 fmt.Sscanf(t, "%x", &n) return n } var from_taddr = func(taddr string) uint32 { n := atoi(taddr) if n == 0 || n > 0x41fffff { panic(fmt.Sprintf("invalid taddr %s", taddr)) } if n <= 0x3ffff { return n | 0x2000000 }else if 0 == (n&0xf000000) { return (n & 0xffff) | 0x3000000 }else { return n } } addrval := make(map[uint32]uint32, 0) for _, command := range *list { if len(command) == 0 { continue } addr := from_taddr(command[0]) len := len(command) - 1 for i := 0; i < len; i++ { addrval[addr + uint32(i)] = atoi(command[i+1]) } } ordered := make([][2]uint32, 0) for addr, val := range addrval { ordered = append(ordered, [2]uint32{addr, val}) } sort.Slice(ordered, func(i, j int) bool { return ordered[i][0] < ordered[j][0] }) blocks, curr := make([]uint32, 0), [3]uint32{0, 0, 0} for _, addrval := range ordered { addr, value := addrval[0], addrval[1] dup[addr] = hole if 0 == curr[2] { curr = [3]uint32{addr, value, 1} }else if curr[1] == value && curr[0] + curr[2] == addr { curr[2]++ }else { blocks = append(blocks, curr[0], curr[2]<<8 | curr[1]) curr = [3]uint32{addr, value, 1} } } if curr[2] > 0 { blocks = append(blocks, curr[0], curr[2]<<8 | curr[1]) } return blocks } func loadlist() ([]Serial, error) { bytes, err := os.ReadFile("./serial.json") if err != nil { fmt.Println(err) return nil, err } var jsonData map[string]string err = json.Unmarshal(bytes, &jsonData) if err != nil { fmt.Println(err) return nil, err } var serials []Serial for k, v := range jsonData { if v != "????" { serials = append(serials, Serial{order: k, serial: v}) } } return serials, nil } func lade(locks []Lock, serial string, order string) Packed { rootstr, names := make([]*string, 0), make([]*string, 0) enttable := make([]EntData, 0) var make_id = func(a uint32, b uint32) uint32 { return a | b<<16 }; for i, lock := range locks { rootstr = append(rootstr, &lock.name) names = append(names, &lock.name) holestr := make([]*string, 0) colname := len(lock.holes) > 1 for idx, hole := range lock.holes { holestr = append(holestr, &hole.name) if colname { names = append(names, &hole.name) } id := make_id(uint32(i+1), uint32(idx+1)) enttable = append( enttable, EntData{ id, EntCmds, make([]*string, 0), &hole.keys, }) } enttable = append(enttable, EntData{ make_id(uint32(i+1), 0), EntNames, holestr, nil, }) } enttable = append(enttable, EntData{ make_id(0, 0), EntNames, rootstr, nil, }) if len(enttable) > 0xffff { panic("too many entries") } sort.Slice(enttable, func(i, j int) bool { return enttable[i].id < enttable[j].id }) sort.Slice(names, func(i, j int) bool { if len(*names[i]) == len(*names[j]) { return *names[i] < *names[j] }else { return len(*names[j]) < len(*names[i]) } }) strtable, idxtable, cmdtable := make([]byte, 0), make([]uint16, 0), make([]uint32, 0) strcache := make(map[string]uint16, 0) var ut_push = func(tr []byte) uint16 { off := len(strtable) strtable = append(strtable, tr...) strtable = append(strtable, 0) return uint16(off) } var ut_addr = func(tr string) uint16 { if v, ok := strcache[tr]; ok { return v } idx := find_index(strtable, []byte(tr)) var off uint16 if idx < 0 { off = ut_push([]byte(tr)) } else { off = uint16(idx) } strcache[tr] = off return off } var add_string = func(list []*string) []uint16 { retval := make([]uint16, len(list)) for i, v := range list { retval[i] = ut_addr(*v) } return retval } var add_str_index = func(list []uint16) uint16 { if len(list) == 0 { return 0 } if len(strtable) > 0xffff { panic("too many strings") } retval := uint16(len(idxtable)) idxtable = append(idxtable, list...) return retval*2 } var add_command = func(list []uint32) uint32 { retval := len(cmdtable) cmdtable = append(cmdtable, list...) return uint32(retval*4) } add_string(names) entbytelist, emptylist, holedup := make([][3]uint32, 0), make([]*string, 0), make(map[uint32]uint16, 0) for _, entry := range enttable { if entry.t == EntNames { var idxlist []uint16 if entry.id == 0 { idxlist = add_string( entry.strs ) }else if len(entry.strs) > 1 { idxlist = add_string( entry.strs ) }else { idxlist = add_string( emptylist ) } location := add_str_index(idxlist) entbytelist = append(entbytelist, [3]uint32{ entry.id, uint32(location), uint32(len(idxlist)), }) } else { armbytecode := assemble_cheat( entry.cmds, holedup, uint16(entry.id & 0xffff) ) location := add_command(armbytecode) entbytelist = append(entbytelist, [3]uint32{ entry.id, uint32(location), uint32(len(armbytecode)), }) } } var code uint32 = 0 for _, c := range []byte(serial) { code = (code << 8) | uint32(c & 0xff) } for _, cmd := range cmdtable { code = code ^ cmd } return Packed{code, pack(entbytelist, strtable, idxtable, cmdtable) } } func pack(entlist [][3]uint32, strlist []byte, idxlist []uint16, cmdlist []uint32) []byte { entrysize := 12 strbase := 2 + len(entlist) * entrysize idxbase := strbase + len(strlist) cmdbase := idxbase + len(idxlist) * 2 nonalign_size := cmdbase + len(cmdlist) * 4 size := align(uint32(nonalign_size), 32) result := make([]byte, size) binary.Encode(result, binary.LittleEndian, uint32(len(entlist))) for i, entry := range entlist { offset := i * entrysize + 2 locbase := idxbase if entry[0] > 0xffff { locbase = cmdbase } binary.Encode(result[offset:], binary.LittleEndian, entry[0]) binary.Encode(result[offset+4:], binary.LittleEndian, entry[1] + uint32(locbase)) binary.Encode(result[offset+8:], binary.LittleEndian, entry[2]) } copy(result[strbase:], strlist) for i, v := range idxlist { binary.Encode(result[idxbase + i*2:], binary.LittleEndian, v) } for i, v := range cmdlist { binary.Encode(result[cmdbase + i*4:], binary.LittleEndian, v) } return result } func parse(datas []byte, serial string, order string) ([]Packed, error) { var retval []Packed var token []byte state, line, locks := WaitLock, 1, make([]Lock, 0) var currlock *Lock var currhole *LockHole var token_str = func() string { var r = string( token ); token = make([]byte, 0) return r; }; var error = func(msg string) { panicmsg := fmt.Sprintf("error occur %s on line %d", msg, line) panic(panicmsg) }; // incr var incr = func(ch byte) { switch ch { case '[': if state == WaitLock || state == ReadHole || state == NeedPart { state = ReadLock; if currlock != nil { locks = append(locks, *currlock ); } currlock = &Lock{"", make([]LockHole, 0)}; }else if state == WaitPart { }else { error(fmt.Sprintf("error occur [ on %d", state) ) } case ']': if state == ReadLock { state = WaitHole name := token_str() if strings.ToLower(name) == "gameinfo" { retval = append(retval, lade(locks, serial, order)) state = WaitPart locks = make([]Lock, 0) currlock = nil currhole = nil }else if len(name) > 0 { currlock = &Lock{name, make([]LockHole, 0)} currhole = nil }else { error(fmt.Sprintf("empty lock name in %s", order) ) } }else if state == WaitPart { }else if state == NeedPart{ state = WaitPart }else { error(fmt.Sprintf("error occur ] on %d", state) ) } case '\r', '\n': if state == WaitHole { state = ReadHole }else if state == ReadKey { t, pass := token_str(), false if currhole != nil { if len(t) > 0 { idx := len(currhole.keys) - 1 currhole.keys[idx] = append(currhole.keys[idx], t) } t = strings.ToLower(currhole.name) if t == "text" || t == "off" { pass = true } } if !pass { if currlock != nil { if currhole == nil { error("unreachable for pass") } currlock.holes = append(currlock.holes, *currhole) //LockHole{name: currhole.name, keys: currhole.keys}) } currhole = nil } state = ReadHole }else if state == ReadLock { error(fmt.Sprintf("error occur newline on %d", state)) }else if state == ReadHole { if len(token) > 0 && bytes.ContainsFunc(token, func(r rune) bool { return r != ' ' && r != '\t' }) { error(fmt.Sprintf("error occur space on %d", state)) } else { token = make([]byte, 0) } }else if state == WaitPart { state = NeedPart } else {} if ch == '\n' { line++ } case '=': if state == ReadHole { state = ReadKey currhole = &LockHole{name: token_str(), keys: make([]LockKey, 1)} }else if state == ReadLock { token = append(token, ch) }else if state == WaitPart { }else if state == NeedPart { state = WaitPart }else { error(fmt.Sprintf("error occur = on %d", state)) } case ',': if state == ReadKey { if currhole == nil { error("unreachable for currhole") } if len(token) > 0 { idx := len(currhole.keys) - 1 currhole.keys[idx] = append(currhole.keys[idx], token_str()) } }else if state == ReadHole { if currlock == nil { error("unreachable for currlock") } idx := len(currlock.holes) - 1 tmp := currlock.holes[idx] currlock.holes = currlock.holes[:idx] if len(token) > 0 { pos := len(tmp.keys) - 1 tmp.keys[pos] = append(tmp.keys[pos], token_str()) } currhole = &LockHole{name: tmp.name, keys: tmp.keys} state = ReadKey }else if state == ReadLock { token = append(token, ch) }else if state == WaitPart { }else if state == NeedPart { state = WaitPart }else { error(fmt.Sprintf("error occur , on %d", state)) } case ';': if state == ReadKey { if currhole == nil { error("unreachable for currhole") } if len(token) > 0 { idx := len(currhole.keys) - 1 currhole.keys[idx] = append(currhole.keys[idx], token_str()) } if currhole != nil { currhole.keys = append(currhole.keys, make([]string, 0)) } }else if state == ReadLock { token = append(token, ch) }else if state == ReadHole { if currlock == nil { error("unreachable for currlock") } idx := len(currlock.holes) - 1 tmp := currlock.holes[idx] currlock.holes = currlock.holes[:idx] if len(token) > 0 { pos := len(tmp.keys) - 1 tmp.keys[pos] = append(tmp.keys[pos], token_str()) } tmp.keys = append(tmp.keys, make([]string, 0)) currhole = &LockHole{name: tmp.name, keys: tmp.keys} state = ReadKey }else if state == WaitPart { }else if state == NeedPart { state = WaitPart }else { error(fmt.Sprintf("error occur ; on %d", state)) } case ' ': if state == ReadLock || state == ReadHole { token = append(token, ch) }else if state == NeedPart { state = WaitPart }else {} default: if state == ReadLock || state == ReadHole { token = append(token, ch) }else if state == ReadKey { if strings.Contains("0123456789abcdefABCDEF", string(ch)) { token = append(token, ch) } else if currhole != nil { if strings.ToLower(currhole.name) == "text" { token = append(token, ch) } } else { error(fmt.Sprintf("error occur %c on %d", ch, state)) } }else if state == WaitPart { }else if state == NeedPart { state = WaitPart } else { error(fmt.Sprintf("error occur %c on %d", ch, state))} } } // done var done = func() { if state != WaitPart && state != NeedPart { error(fmt.Sprintf("unexpected end of file %d in %s", state, order)) } } for _, ch := range datas { incr(ch) } done() return retval, nil } func transform(list []Serial) []Cheat { var retval []Cheat for _, game := range list { file := fmt.Sprintf("./gba/%s.u8", game.order) if _, err := os.Stat(file); os.IsNotExist(err) { // fmt.Println(file, " not found") retval = append( retval, Cheat{ game.serial, make([]Packed, 0) } ) continue } bytes, err := os.ReadFile(file) if err != nil || len(bytes) == 0 { retval = append( retval, Cheat{ game.serial, make([]Packed, 0) } ) } else { cheats, err := parse(bytes, game.serial, game.order) if err != nil { fmt.Println(err) retval = append( retval, Cheat{ game.serial, make([]Packed, 0) } ) } else { retval = append( retval, Cheat{ game.serial, cheats } ) } } } return retval } func format( cheats []Cheat ) []byte { sort.Slice( cheats, func(i, j int) bool { return cheats[i].serial < cheats[j].serial }) fmt.Println("valid rom has", len(cheats)) var step1 = func() ([]byte, []uint16, uint32, uint32) { item1, item2 := make([]byte, 0), make([]uint16, 0) item3, item4, item5, last := uint32(0), uint32(0), "", uint32(0) for _, ch := range cheats { serial, cheat := ch.serial, ch.cheats val := serial[0:3] if item5 != val { if len(item2) > 0 && item3 - last > item4 { item4 = item3 - last } item1 = append(item1, []byte(val)...) item2 = append(item2, uint16(item3)) last = item3 item5 = val } item3 += uint32(len(cheat)) } if len(item2) > 0 { if item3 - last > item4 { item4 = item3 - last } item2 = append(item2, uint16(item3)) } return item1, item2, item3, item4 } sers, offs, chtc, maxl := step1() var step2 = func() ([]uint32, []byte) { item1, item2 := make([]uint32, 0), make([]byte, 0) item3 := align(uint32(8 + len(sers) + len(offs)*2 + int(chtc) * 8), 32) for _, ch := range cheats { serial, cheat := ch.serial, ch.cheats val := serial[3] for _, cht := range cheat { id, bin := cht.code, cht.data off := item3 + uint32(len(item2)) item1 = append(item1, uint32(val) | (off<<3), id) item2 = append(item2, bin...) } } return item1, item2 } chts, expanded := step2() fmt.Printf("name: %d cheats: %d maxl: %d", len(sers), chtc, maxl) serialbase := 8 offsetbase := serialbase + len(sers) cheatbase := offsetbase + len(offs) * 2 cheatend := cheatbase + len(chts) * 4 expandbase := align(uint32(cheatend), 32) total := int(expandbase) + len(expanded) retval := make([]byte, total) retval[0] = 'A' retval[1] = 'C' retval[2] = 'L' retval[3] = 1 binary.Encode(retval[4:], binary.LittleEndian, uint16(len(sers)/3)) binary.Encode(retval[6:], binary.LittleEndian, uint16(maxl)) copy(retval[serialbase:], sers) for i, off := range offs { binary.Encode(retval[offsetbase+i*2:], binary.LittleEndian, off) } for i, ch := range chts { binary.Encode(retval[cheatbase+i*4:], binary.LittleEndian, ch) } copy(retval[cheatbase:], expanded) return retval } func main() { begin := time.Now() list, err := loadlist() if err != nil { fmt.Println(err) return } roms := transform(list) fmt.Println("all rom has", len(roms), time.Now().Sub(begin).Milliseconds()) idx := make(map[string][]Packed, 0) for _, rom := range roms { if len(rom.cheats) == 0 { continue } if _, ok := idx[rom.serial]; !ok { idx[rom.serial] = make([]Packed, 0) } idx[rom.serial] = append(idx[rom.serial], rom.cheats...) } cheats := make([]Cheat, 0) for serial, chts := range idx { cheats = append(cheats, Cheat{ serial, chts }) } content := format( cheats ) os.WriteFile("gba.acl", content, 0644) }