mirror of
https://gitee.com/anod/open_agb_firm.git
synced 2025-05-06 05:44:11 +08:00
648 lines
26 KiB
Kotlin
648 lines
26 KiB
Kotlin
// build command: kotlinc -cp json-simple.jar -Xinline-classes -include-runtime -jvm-target=20 -d mkacl.jar mkacl.kt
|
||
// run command: java -cp "json-simple.jar:kotlin-stdlib.jar:mkacl.jar" MkaclKt
|
||
// java的内存blt确实麻烦,计算速度也比较慢,生成内容就没做验证了,反正真的慢
|
||
|
||
import org.json.simple.*
|
||
import java.nio.file.*
|
||
import java.nio.*
|
||
import java.util.*
|
||
import kotlin.system.*
|
||
import kotlin.time.*
|
||
|
||
enum class ParserState {
|
||
WaitLock,
|
||
ReadLock,
|
||
WaitHole,
|
||
ReadHole,
|
||
ReadKey,
|
||
WaitPart,
|
||
NeedPart,
|
||
}
|
||
|
||
typealias LockKey = ArrayList<String>
|
||
data class LockHole(val name: String, var keys: ArrayList<LockKey>)
|
||
data class Lock(val name: String, var holes: ArrayList<LockHole>)
|
||
data class CheatMap(val serial: String, val order: String)
|
||
data class CheatSet(val code: UInt, val sets: ByteArray)
|
||
data class CheatGrp(val serial: String, val grp: ArrayList<CheatSet>)
|
||
data class Entry(val id: UInt, val isKeys: Boolean, val strs: ArrayList<String>?, var keys: ArrayList<LockKey>? )
|
||
data class EntData(val id:UInt, val loc: UInt, val len: UInt)
|
||
data class AsData(val addr:UInt, val value: UInt);
|
||
|
||
val PassHole = arrayListOf("off", "text")
|
||
|
||
fun findIndex(t: ArrayList<UByte>, tr: ByteArray): Int {
|
||
val rlen = tr.size
|
||
val tlen = t.size
|
||
val limit = tlen - rlen
|
||
for (i in 0..<limit) {
|
||
if (t[i+rlen].toUInt() != 0u) continue;
|
||
|
||
var eq = true
|
||
for (j in 0..<rlen) {
|
||
if (t[i+j].toByte() != tr[j]) {
|
||
eq = false
|
||
break
|
||
}
|
||
}
|
||
if (eq) return i
|
||
}
|
||
return -1
|
||
}
|
||
|
||
fun loadList(): ArrayList<CheatMap> {
|
||
// 读取文件内容
|
||
val jsonContent = Files.readAllBytes( Paths.get("serial.json") )
|
||
val jsonText = String(jsonContent)
|
||
|
||
// 解析 JSON
|
||
val jso = JSONValue.parse(jsonText) as JSONObject
|
||
var list = ArrayList<CheatMap>()
|
||
for (entry in jso) {
|
||
val order = entry.key as String
|
||
val serial = entry.value as String
|
||
if (serial != "????") {
|
||
list.add(CheatMap(serial, order))
|
||
}
|
||
}
|
||
return list
|
||
}
|
||
|
||
fun transform( list: ArrayList<CheatMap> ): ArrayList<CheatGrp> {
|
||
var retval = ArrayList<CheatGrp>()
|
||
for (item in list) {
|
||
val file = "./gba/${item.order}.u8"
|
||
if (!Files.exists(Paths.get(file))) {
|
||
retval.add(CheatGrp(item.serial, ArrayList()))
|
||
}
|
||
else {
|
||
val chtdata = Files.readAllBytes( Paths.get(file) );
|
||
if (chtdata.size > 0) {
|
||
var cheats = parse(chtdata, item.serial, item.order);
|
||
retval.add(CheatGrp(item.serial, cheats))
|
||
}
|
||
else retval.add(CheatGrp(item.serial, ArrayList()))
|
||
}
|
||
}
|
||
return retval
|
||
}
|
||
|
||
inline fun makeID( a:UInt, b: UInt): UInt = a or (b shl 16) // 令人窒息的代码!
|
||
|
||
inline fun align( n: UInt, base: UInt ): UInt = (n + base - 1u) and (base - 1u).inv()
|
||
|
||
fun isSpace(tr: ArrayList<Char>): Boolean {
|
||
var res = false
|
||
for (ch in tr) {
|
||
if (ch != ' ' && ch != '\t'){
|
||
res = true
|
||
break
|
||
}
|
||
}
|
||
return res
|
||
}
|
||
fun parse(data: ByteArray, serial: String, order: String): ArrayList<CheatSet> {
|
||
var retval = ArrayList<CheatSet>()
|
||
var state = ParserState.WaitLock
|
||
var token = ArrayList<Char>()
|
||
var line = 1
|
||
var locks = ArrayList<Lock>()
|
||
var currlock: Lock? = null
|
||
var currhole: LockHole? = null
|
||
|
||
fun tokenStr(): String {
|
||
val retval = String( token.toCharArray() )
|
||
token.clear()
|
||
return retval
|
||
}
|
||
|
||
fun error(msg: String) {
|
||
println("Error: $msg at line $line in $order")
|
||
exitProcess(1)
|
||
}
|
||
|
||
fun incr( ch: Char ) {
|
||
when(ch) {
|
||
'[' -> {
|
||
if (state == ParserState.WaitLock || state == ParserState.ReadHole || state == ParserState.NeedPart) {
|
||
state = ParserState.ReadLock
|
||
if (currlock != null) {
|
||
locks.add(currlock!!)
|
||
}
|
||
currlock = Lock("", ArrayList())
|
||
}
|
||
else if (state == ParserState.WaitPart) {}
|
||
else error("error occur [ on $state")
|
||
}
|
||
']' -> {
|
||
if (state == ParserState.ReadLock){
|
||
state = ParserState.WaitHole
|
||
val name = tokenStr()
|
||
if (name.lowercase() == "gameinfo"){
|
||
retval.add( lade(locks, serial, order) );
|
||
state = ParserState.WaitPart
|
||
locks = ArrayList()
|
||
currhole = null
|
||
currlock = null
|
||
}
|
||
else if (name.length > 0) {
|
||
currlock = Lock(name, ArrayList())
|
||
currhole = null
|
||
}
|
||
else error("empty lock name in $order")
|
||
}
|
||
else if (state == ParserState.WaitPart) {}
|
||
else if (state == ParserState.NeedPart) {
|
||
state = ParserState.WaitPart
|
||
}
|
||
else error("error occur ] on $state")
|
||
}
|
||
'\r', '\n' -> {
|
||
if (state == ParserState.WaitHole) {
|
||
state = ParserState.ReadHole
|
||
}
|
||
else if (state == ParserState.ReadKey) {
|
||
val token = tokenStr()
|
||
var pass = false
|
||
if (currhole != null) {
|
||
if (token.length > 0) {
|
||
currhole!!.keys.get(currhole!!.keys.size - 1).add(token)
|
||
}
|
||
|
||
if (PassHole.contains(currhole!!.name.lowercase())) {
|
||
pass = true
|
||
}
|
||
}
|
||
if (!pass) {
|
||
if (currlock != null) {
|
||
currlock!!.holes.add(currhole!!)
|
||
}
|
||
currhole = null
|
||
}
|
||
state = ParserState.ReadHole
|
||
}
|
||
else if (state == ParserState.ReadLock) {
|
||
error("error occur newline on $state")
|
||
}
|
||
else if (state == ParserState.ReadHole) {
|
||
if (token.size > 0 && isSpace(token)) {
|
||
error("error occur newline on $state")
|
||
}
|
||
else token.clear()
|
||
}
|
||
else if (state == ParserState.WaitPart) {
|
||
state = ParserState.NeedPart
|
||
}
|
||
else {}
|
||
if (ch == '\n') ++line
|
||
}
|
||
'=' -> {
|
||
if (state == ParserState.ReadHole) {
|
||
state = ParserState.ReadKey
|
||
currhole = LockHole(tokenStr(), arrayListOf( LockKey() ))
|
||
}
|
||
else if (state == ParserState.ReadLock) {
|
||
token.add(ch)
|
||
}
|
||
else if (state == ParserState.WaitPart) {}
|
||
else if (state == ParserState.NeedPart) {
|
||
state = ParserState.WaitPart
|
||
}
|
||
else error("error occur = on $state")
|
||
}
|
||
',' -> {
|
||
if (state == ParserState.ReadKey) {
|
||
if (token.size > 0) {
|
||
currhole!!.keys.get(currhole!!.keys.size - 1).add(tokenStr())
|
||
}
|
||
}
|
||
else if (state == ParserState.ReadHole) {
|
||
var t = currlock!!.holes.removeAt(currlock!!.holes.size - 1)
|
||
if (token.size > 0) t.keys.get(t.keys.size - 1).add(tokenStr())
|
||
currhole = t
|
||
state = ParserState.ReadKey
|
||
}
|
||
else if (state == ParserState.ReadLock) {
|
||
token.add(ch)
|
||
}
|
||
else if (state == ParserState.WaitPart) {}
|
||
else if (state == ParserState.NeedPart) {
|
||
state = ParserState.WaitPart
|
||
}
|
||
else error("error occur , on $state")
|
||
}
|
||
';' -> {
|
||
if (state == ParserState.ReadKey) {
|
||
if (token.size > 0) currhole!!.keys.get(currhole!!.keys.size - 1).add(tokenStr())
|
||
currhole!!.keys.add(LockKey())
|
||
}
|
||
else if (state == ParserState.ReadLock) {
|
||
token.add(ch)
|
||
}
|
||
else if (state == ParserState.ReadHole) {
|
||
var t = currlock!!.holes.removeAt(currlock!!.holes.size - 1)
|
||
if (token.size > 0) t.keys.get(t.keys.size - 1).add(tokenStr())
|
||
t.keys.add(LockKey())
|
||
currhole = t
|
||
state = ParserState.ReadKey
|
||
}
|
||
else if (state == ParserState.WaitPart) {}
|
||
else if (state == ParserState.NeedPart) {
|
||
state = ParserState.WaitPart
|
||
}
|
||
else error("error occur ; on $state")
|
||
}
|
||
' ' -> {
|
||
if (state == ParserState.ReadLock || state == ParserState.ReadHole) {
|
||
token.add(ch)
|
||
}
|
||
else if (state == ParserState.NeedPart) {
|
||
state = ParserState.WaitPart
|
||
}
|
||
else {}
|
||
}
|
||
else -> {
|
||
if (state == ParserState.ReadLock || state == ParserState.ReadHole) {
|
||
token.add(ch)
|
||
}
|
||
else if (state == ParserState.ReadKey) {
|
||
if ( "0123456789abcdefABCDEF".indexOf(ch) >= 0) {
|
||
token.add(ch)
|
||
}
|
||
else if (currhole != null) {
|
||
if (currhole!!.name == "text") {
|
||
token.add(ch)
|
||
}
|
||
}
|
||
else error("error occur $ch on $state")
|
||
}
|
||
else if (state == ParserState.WaitPart) {}
|
||
else if (state == ParserState.NeedPart) {
|
||
state = ParserState.WaitPart
|
||
}
|
||
else error("error occur $ch on $state")
|
||
}
|
||
}
|
||
}
|
||
|
||
fun done() {
|
||
if (state != ParserState.WaitPart && state != ParserState.NeedPart) {
|
||
error("Unexpected end of file")
|
||
}
|
||
}
|
||
|
||
val data = String(data, Charsets.UTF_8)
|
||
for (ch in data) {
|
||
incr(ch)
|
||
}
|
||
done()
|
||
|
||
return retval
|
||
}
|
||
|
||
fun assembleCheat( list: ArrayList<LockKey>, dup: TreeMap<UInt, UShort>, hole: UShort, order: String): ArrayList<UInt> {
|
||
fun fromTaddr(taddr: String): UInt {
|
||
val n = taddr.toInt(16)
|
||
if (n == 0 || n > 0x41fffff) {
|
||
println("get an invalid address $taddr in $order")
|
||
exitProcess(1)
|
||
}
|
||
|
||
if (n <= 0x3ffff) {
|
||
return n.toUInt() or 0x2000000u
|
||
}
|
||
else if (0 == (n and 0xf000000)) {
|
||
return (n.toUInt() and 0xffffu) or 0x3000000u
|
||
}
|
||
else return n.toUInt()
|
||
}
|
||
var addrval = TreeMap<UInt, UInt>()
|
||
for (command in list) {
|
||
if (command.size == 0){
|
||
continue
|
||
}
|
||
|
||
val addr = fromTaddr(command[0])
|
||
var len = command.size - 1
|
||
for (pos in 0..<len) {
|
||
addrval[addr + pos.toUInt()] = command[pos + 1].toUInt(16)
|
||
}
|
||
}
|
||
val ordered = ArrayList<AsData>()
|
||
ordered.ensureCapacity(addrval.size)
|
||
addrval.forEach {
|
||
ordered.add(AsData(it.key, it.value))
|
||
}
|
||
ordered.sortBy { it.addr }
|
||
|
||
var blocks = ArrayList<UInt>()
|
||
var curr = arrayOf(0u, 0u, 0u)
|
||
for (asd in ordered) {
|
||
dup[asd.addr] = asd.value.toUShort()
|
||
|
||
if (curr[2] == 0u) {
|
||
curr[0] = asd.addr
|
||
curr[1] = asd.value
|
||
curr[2] = 1u
|
||
}
|
||
else {
|
||
if (curr[1] == asd.value && curr[0] + curr[2] == asd.addr) {
|
||
curr[2] += 1u
|
||
}
|
||
else {
|
||
blocks.add(curr[0])
|
||
blocks.add( (curr[2] shl 8) or curr[1] )
|
||
curr[0] = asd.addr
|
||
curr[1] = asd.value
|
||
curr[2] = 1u
|
||
}
|
||
}
|
||
}
|
||
if (curr[2] != 0u) {
|
||
blocks.add( curr[0] )
|
||
blocks.add( (curr[2] shl 8) or curr[1] )
|
||
}
|
||
|
||
return blocks
|
||
}
|
||
|
||
fun lade(locks: ArrayList<Lock>, serial: String, order: String): CheatSet {
|
||
var rootstr = ArrayList<String>()
|
||
var names = ArrayList<String>()
|
||
var enttable = ArrayList<Entry>()
|
||
|
||
for ((i, lock) in locks.withIndex()) {
|
||
rootstr.add(lock.name)
|
||
names.add(lock.name)
|
||
|
||
var holestr = ArrayList<String>()
|
||
var colname = lock.holes.size > 1;
|
||
for ((index, hole) in lock.holes.withIndex()) {
|
||
holestr.add(hole.name)
|
||
if (colname) names.add(hole.name)
|
||
val id = makeID(i.toUInt() + 1u, index.toUInt() + 1u)
|
||
enttable.add(Entry(id, true, null, hole.keys))
|
||
}
|
||
val id = makeID(i.toUInt() + 1u, 0u)
|
||
enttable.add(Entry(id, false, holestr, null))
|
||
}
|
||
enttable.add(Entry(0u, false, rootstr, null))
|
||
|
||
if (enttable.size > 0xffff) {
|
||
println("entry count is overflow in file $order")
|
||
exitProcess(1)
|
||
}
|
||
|
||
enttable.sortBy { it.id }
|
||
names.sortWith( comparator = { a, b -> if (a.length == b.length) { a.compareTo(b) } else { b.length - a.length } })
|
||
|
||
var strtable = ArrayList<UByte>()
|
||
var idxtable = ArrayList<UShort>()
|
||
var cmdtable = ArrayList<UInt>()
|
||
var strcache = HashMap<String, UShort>()
|
||
|
||
fun utpush( tr: ByteArray ): UShort {
|
||
val off = strtable.size
|
||
strtable.ensureCapacity(off + tr.size + 1)
|
||
strtable.addAll( arrayListOf(*tr.map { it.toUByte() }.toTypedArray()) )
|
||
strtable.add(0u.toUByte())
|
||
return off.toUShort()
|
||
}
|
||
fun utaddr( tr: String ): UShort {
|
||
if (strcache.containsKey(tr)) {
|
||
return strcache[tr]!!
|
||
}
|
||
else {
|
||
val codes = tr.toByteArray(Charsets.UTF_8)
|
||
var idx = findIndex(strtable, codes)
|
||
var off:UShort = if (idx < 0) { utpush(codes) } else { idx.toUShort() }
|
||
strcache[tr] = off
|
||
return off
|
||
}
|
||
}
|
||
|
||
fun addString(list: ArrayList<String>): Array<UShort> {
|
||
var ret: Array<UShort> = Array<UShort>(list.size) {0u}
|
||
for ((i,s) in list.withIndex()) {
|
||
ret[i] = utaddr(s)
|
||
}
|
||
return ret
|
||
}
|
||
|
||
fun addStrIndex(list: Array<UShort>) : UShort {
|
||
if (list.size == 0) return 0u
|
||
|
||
if (strtable.size > 0xffff) {
|
||
println("String table overflow $order")
|
||
}
|
||
|
||
var ret = idxtable.size
|
||
idxtable.ensureCapacity(ret + list.size)
|
||
idxtable.addAll( list )
|
||
return (ret * 2).toUShort()
|
||
}
|
||
|
||
fun addCommand( list: ArrayList<UInt> ): UInt {
|
||
var ret = cmdtable.size
|
||
cmdtable.ensureCapacity( ret + list.size )
|
||
cmdtable.addAll( list )
|
||
return (ret * 4).toUInt()
|
||
}
|
||
|
||
addString(names)
|
||
|
||
var entbytelist = ArrayList<EntData>()
|
||
var emptylist = ArrayList<String>()
|
||
var holedup = TreeMap<UInt, UShort>()
|
||
for (entry in enttable) {
|
||
val id = entry.id
|
||
if (entry.isKeys) {
|
||
val armbytecode = assembleCheat( entry.keys!!, holedup, (id and 0xffffu).toUShort(), order )
|
||
val location = addCommand( armbytecode )
|
||
entbytelist.add( EntData(id, location, armbytecode.size.toUInt()) )
|
||
}
|
||
else {
|
||
val idxlist = addString( if (id==0u) { entry.strs!! } else if (entry.strs!!.size > 1) { entry.strs!! } else { emptylist} )
|
||
val location = addStrIndex( idxlist )
|
||
entbytelist.add( EntData( id, location.toUInt(), idxlist.size.toUInt()) )
|
||
}
|
||
}
|
||
|
||
var code = 0u
|
||
for (ch in serial) {
|
||
code = (code shl 8) or (ch.code.toUInt() and 0xffu)
|
||
}
|
||
for (cmd in cmdtable) {
|
||
code = code xor cmd
|
||
}
|
||
|
||
return CheatSet(code, pack(entbytelist, strtable, idxtable, cmdtable))
|
||
}
|
||
|
||
fun pack( entlist: ArrayList<EntData>, strlist: ArrayList<UByte>, idxlist: ArrayList<UShort>, cmdlist: ArrayList<UInt>): ByteArray {
|
||
val entrysize = 12
|
||
val strbase = 2 + entlist.size * entrysize
|
||
val idxbase = strbase + strlist.size
|
||
val cmdbase = idxbase + idxlist.size * 2
|
||
val datasize = cmdbase + cmdlist.size * 4
|
||
val size = align(datasize.toUInt(), 32u)
|
||
|
||
var ret = ByteArray(size.toInt())
|
||
var mem = ByteBuffer.wrap(ret)
|
||
mem.order(ByteOrder.LITTLE_ENDIAN)
|
||
mem.putShort( 0, entlist.size.toShort() )
|
||
|
||
for ((i, entry) in entlist.withIndex()) {
|
||
val offset = 2 + i * entrysize
|
||
val locbase = if (entry.id > 0xffffu) { cmdbase } else { idxbase}
|
||
mem.putInt(offset, entry.id.toInt())
|
||
mem.putInt(offset+4, locbase + entry.loc.toInt())
|
||
mem.putInt(offset+8, entry.len.toInt())
|
||
}
|
||
|
||
// copy
|
||
var mempos = strbase
|
||
strlist.forEach {
|
||
mem.put(mempos, it.toByte())
|
||
mempos += 1
|
||
}
|
||
idxlist.forEach {
|
||
mem.putShort(mempos, it.toShort())
|
||
mempos += 2
|
||
}
|
||
cmdlist.forEach {
|
||
mem.putInt(mempos, it.toInt())
|
||
mempos += 4
|
||
}
|
||
|
||
return ret;
|
||
}
|
||
|
||
fun format( cheats: ArrayList<CheatGrp> ): ByteArray {
|
||
cheats.sortBy { it.serial }
|
||
println("valid rom has ${cheats.size}")
|
||
|
||
var sers: ArrayList<Byte>? = null
|
||
var offs: ArrayList<Short>? = null
|
||
var chtc: Int? = null
|
||
var maxl: Int? = null
|
||
fun step1()
|
||
{
|
||
var item1 = ArrayList<Byte>()
|
||
var item2 = ArrayList<Short>()
|
||
var item3 = 0
|
||
var item4 = 0
|
||
var item5 = ""
|
||
var last = 0
|
||
for (c in cheats) {
|
||
val value = c.serial.substring(0, 3)
|
||
if (item5 != value) {
|
||
if (item2.size > 0 && item3 - last > item4) {
|
||
item4 = item3 - last
|
||
}
|
||
val bytes = value.toByteArray()
|
||
item1.add( bytes[0] )
|
||
item1.add( bytes[1] )
|
||
item1.add( bytes[2] )
|
||
item2.add( item3.toShort() )
|
||
last = item3
|
||
item5 = value
|
||
}
|
||
item3 += c.grp.size
|
||
}
|
||
if (item2.size > 0) {
|
||
if (item3 - last > item4) {
|
||
item4 = item3 - last
|
||
}
|
||
item2.add( item3.toShort() )
|
||
}
|
||
sers = item1
|
||
offs = item2
|
||
chtc = item3
|
||
maxl = item4
|
||
}
|
||
step1()
|
||
|
||
var chts: ArrayList<Int>? = null
|
||
var expanded: ArrayList<Byte>? = null
|
||
fun step2() {
|
||
var item1 = ArrayList<Int>()
|
||
var item2 = ArrayList<Byte>()
|
||
val item3 = align( (8 + sers!!.size + offs!!.size*2 + chtc!! * 8).toUInt(), 32u)
|
||
for (c in cheats) {
|
||
var value = Character.codePointAt(c.serial, 3) and 0xFF
|
||
for (cht in c.grp) {
|
||
val off = item3.toInt() + item2.size
|
||
item1.add( value or (off shl 3) )
|
||
item1.add( cht.code.toInt() )
|
||
item2.ensureCapacity( item2.size + cht.sets.size )
|
||
for (t in 0..<cht.sets.size) {
|
||
item2.add( cht.sets[t] )
|
||
}
|
||
}
|
||
}
|
||
chts = item1
|
||
expanded = item2
|
||
}
|
||
step2()
|
||
println("name: ${sers!!.size} cheats: $chtc maxl: $maxl")
|
||
|
||
val serialbase = 8
|
||
val offsetbase = serialbase + sers!!.size
|
||
val cheatbase = offsetbase + offs!!.size * 2
|
||
val cheatend = cheatbase + chts!!.size * 4
|
||
val expandbase = align(cheatend.toUInt(), 32u).toInt()
|
||
val total = expandbase + expanded!!.size
|
||
|
||
var retval = ByteArray(total)
|
||
retval[0] = Character.codePointAt("ACL", 0).toByte()
|
||
retval[1] = Character.codePointAt("ACL", 1).toByte()
|
||
retval[2] = Character.codePointAt("ACL", 2).toByte()
|
||
retval[3] = 1
|
||
|
||
var mem = ByteBuffer.wrap(retval, 4, total - 4)
|
||
mem.putShort( (sers!!.size / 3).toShort() )
|
||
mem.putShort( maxl!!.toShort() )
|
||
|
||
var mempos = serialbase
|
||
sers!!.forEach {
|
||
mem.put( mempos, it.toByte() )
|
||
mempos++
|
||
}
|
||
mempos = offsetbase
|
||
offs!!.forEach {
|
||
mem.putShort( mempos, it.toShort() )
|
||
mempos += 2
|
||
}
|
||
mempos = cheatbase
|
||
chts!!.forEach {
|
||
mem.put( mempos, it.toByte() )
|
||
mempos++
|
||
}
|
||
|
||
return retval
|
||
}
|
||
|
||
fun main() {
|
||
val tsrc = TimeSource.Monotonic
|
||
val start = tsrc.markNow()
|
||
val list = loadList()
|
||
val roms = transform( list );
|
||
println("all rom has ${roms.size} and time ${start.elapsedNow()}")
|
||
var idx = TreeMap<String, ArrayList<CheatSet>>()
|
||
roms.forEach {
|
||
if (it.grp.size == 0) return@forEach
|
||
|
||
if (idx.containsKey(it.serial) == false) {
|
||
idx[it.serial] = ArrayList<CheatSet>()
|
||
}
|
||
idx[it.serial]!!.addAll(it.grp)
|
||
}
|
||
|
||
var nlist = ArrayList<CheatGrp>()
|
||
idx.forEach {
|
||
nlist.add( CheatGrp(it.key, it.value) )
|
||
}
|
||
val content = format( nlist )
|
||
Files.write(Paths.get("gba.acl"), content)
|
||
} |