diff --git a/tools/cheat-builder/make-acl.go b/tools/cheat-builder/make-acl.go new file mode 100644 index 0000000..1b3bc65 --- /dev/null +++ b/tools/cheat-builder/make-acl.go @@ -0,0 +1,672 @@ +package main; + +import ( + "fmt" + "os" + "encoding/json" + "encoding/binary" + "strings" + "bytes" + "sort" +) + +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) + for _, v := range tr { + strtable = append(strtable, v) + } + 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) } + //uint16(idx < 0 ? ut_push([]byte(tr)) : 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)) + for _, v := range list { + idxtable = append(idxtable, v) + } + return retval*2 + } + var add_command = func(list []uint32) uint32 { + retval := len(cmdtable) + for _, v := range list { + cmdtable = append(cmdtable, v) + } + 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() { + list, err := loadlist() + if err != nil { + fmt.Println(err) + return + } + + roms := transform(list) + fmt.Println("all rom has", len(roms)) + + 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) +} \ No newline at end of file