diff --git a/tools/cheat-builder/make-acl.cs b/tools/cheat-builder/make-acl.cs new file mode 100644 index 0000000..62df0ed --- /dev/null +++ b/tools/cheat-builder/make-acl.cs @@ -0,0 +1,634 @@ +using System; +using System.IO; +using System.Text.Json; +using System.Linq; +using System.Collections; +using System.Text; +using System.Runtime.InteropServices; + + +namespace CheatTest; + +using Module = Memory; +using LockKey = List; +using LockHole = (string, List< List >); + +struct Lock { + public string name; + public List holes; +} + +enum ParserState { + WaitLock, + ReadLock, + WaitHole, + ReadHole, + ReadKey, + WaitPart, + NeedPart, +} +enum EntType { + Names, + Cmds, +} + +class Program { + static string[] PASSHOLE = { "off", "text" }; + + static uint Align(uint n, uint basic) { + var ext = n % basic; + return ext == 0 ? n : n + basic - ext; + } + + static int FindIndex( List t, byte[] tr ) { + var rlen = tr.Length; + var tlen = t.Count; + var limit = tlen - rlen; + for( var i=0; i < limit; ++i ){ + if( t[i+rlen] != 0 ) continue; + + var eq = true; + for( var j=0; j < rlen; ++j ){ + if( t[i+j] != tr[j] ){ + eq = false; + break; + } + } + if( eq ) return i; + } + return -1; + } + + static List AssembleCheat( List list, Dictionary dup, ushort hole ){ + Func fromTaddr = (string taddr) => { + var n = Convert.ToUInt32(taddr, 16); + if( n == 0 || n > 0x41fffff) { + Console.WriteLine($"get an invalid address {taddr}"); + Environment.Exit(-1); + } + + if( n <= 0x3ffff ){ + return n | 0x2000000; + } + else if( 0 == (n&0xf000000) ){ + return (n & 0xffff) | 0x3000000; + } + else return n; + }; + + var addrval = new Dictionary(); + foreach( var command in list ){ + if( command.Count == 0 ){ + continue; + } + var addr = fromTaddr( command[0] ); + var len = command.Count - 1; + for( uint pos = 0; pos < len; ++pos ){ + addrval[addr+pos] = Convert.ToUInt32(command[(int)pos+1], 16); + } + } + + var ordered = addrval.Select(kvp => (kvp.Key, kvp.Value)).ToList(); + ordered.Sort( + ( (uint,uint) a, (uint,uint) b ) => { + return (int)a.Item1 - (int)b.Item1; + } + ); + var blocks = new List(); + (uint, uint, uint) curr = (0,0,0); + foreach( var (addr, value) in ordered){ + dup.TryAdd(addr, hole); + + if( curr.Item3 == 0 ){ + curr = (addr, value, 1); + } + else { + if( curr.Item2 == value && curr.Item1 + curr.Item3 == addr ){ + ++curr.Item3; + } + else { + blocks.Add( curr.Item1 ); + blocks.Add( (curr.Item3<<8) | curr.Item2 ); + curr = (addr, value, 1); + } + } + } + if( curr.Item3 != 0 ){ + blocks.Add( curr.Item1 ); + blocks.Add( (curr.Item3<<8) | curr.Item2 ); + } + return blocks; + } + + static (uint, Memory) Lade( List locks, string serial, string order ) { + var rootstr = new List(); + var names = new List(); + var enttable = new List<(uint, EntType, List?, List?)>(); + + Func makeID = (uint a, uint b) => a | (b << 16); + + for (var i = 0; i < locks.Count; ++i){ + Lock l = locks[i]; + rootstr.Add(l.name); + names.Add(l.name); + + var holestr = new List(); + bool colname = l.holes.Count > 1; + for (var index=0; index < l.holes.Count; ++index){ + var (name, cmd) = l.holes[index]; + holestr.Add(name); + if (colname) names.Add(name); + var id = makeID((uint)(i + 1), (uint)(index + 1)); + enttable.Add( (id, EntType.Cmds, null, cmd) ); + } + enttable.Add( (makeID((uint)(i + 1), 0), EntType.Names, holestr, null) ); + } + enttable.Add( (makeID(0, 0), EntType.Names, rootstr, null) ); + + if(enttable.Count > 0xffff) { + throw new Exception($"Too many entries in file {order}"); + } + + enttable = enttable.OrderBy(n => n.Item1).ToList(); + names.Sort( (string a, string b) => { + if( a.Length == b.Length ){ + return a.CompareTo(b); + } + else return b.Length - a.Length; + } ); + + List strtable = new List(1024); + List idxtable = new List(128); + List cmdtable = new List(2048); + + var strcache = new Dictionary(); + Func ut_push = (byte[] tr) => { + var off = strtable.Count; + strtable.EnsureCapacity( off + tr.Length + 1 ); + strtable.AddRange( tr ); + strtable.Add(0); + return (ushort)off; + }; + Func ut_addr = (string tr) => { + if( strcache.ContainsKey(tr) ){ + return (ushort)strcache[tr]; + } + else { + var code = Encoding.UTF8.GetBytes(tr); + var idx = FindIndex(strtable, code); + ushort off = (ushort)(idx < 0 ? ut_push(code) : idx); + strcache[tr] = off; + return off; + } + }; + + Func, ushort[]> addString = (List list) => { + var retval = new ushort[list.Count]; + for( var i=0; i < list.Count; i++ ){ + retval[i] = ut_addr(list[i]); + } + return retval; + }; + + Func addStrIndex = (ushort[] list) => { + if( list.Length == 0 ) return 0; + + if( strtable.Count > 0xffff ){ + Console.WriteLine($"string table is too big {order}"); + } + + var ret = idxtable.Count; + idxtable.EnsureCapacity( ret + list.Length ); + idxtable.AddRange( list ); + + return (ushort)(ret * 2); + }; + + Func, uint> addCommand = (List list) => { + var ret = cmdtable.Count; + cmdtable.EnsureCapacity( ret + list.Count ); + cmdtable.AddRange( list ); + return (uint)(ret * 4); + }; + + addString(names); + + var entbytelist = new List<(uint, uint, uint)>(); + var emptylist = new List(); + var holedup = new Dictionary(); + foreach( var (id, type, labels, cmds) in enttable ){ + if( type == EntType.Names ){ + var idxlist = addString( id == 0 ? labels : labels.Count > 1 ? labels : emptylist); + var location = addStrIndex( idxlist ); + entbytelist.Add(((uint, uint, uint))(id, location, idxlist.Length)); + } + else { + var armbytecode = AssembleCheat( cmds, holedup, (ushort)(id & 0xffff)); + var location = addCommand( armbytecode ); + entbytelist.Add(((uint, uint, uint))(id, location, armbytecode.Count)); + } + } + + uint code = 0; + foreach( var ch in serial ){ + code = (code<<8) | ((uint)ch & 0xff); + } + foreach( var cmd in cmdtable ){ + code = code ^ cmd; + } + return (code, Pack( entbytelist, strtable, idxtable, cmdtable )); + } + + static Memory Pack( List<(uint,uint,uint)> entlist, List strlist, List idxlist, List cmdlist ){ + uint entrysize = 12; + uint strbase = (uint)(2 + entlist.Count * entrysize); + uint idxbase = (uint)(strbase + strlist.Count); + uint cmdbase = (uint)(idxbase + idxlist.Count*2); + uint nonalign_size = (uint)(cmdbase + cmdlist.Count * 4); + uint size = Align(nonalign_size, 32); + + var result = new Memory(new byte[size]); + var span = result.Span; + MemoryMarshal.Write( span.Slice(0, 2), (ushort)entlist.Count ); + + for(var i=0; i < entlist.Count; ++i){ + var entry = entlist[i]; + var offset = 2 + i * entrysize; + var locbase = entry.Item1 > 0xffff ? cmdbase : idxbase; + MemoryMarshal.Write( span.Slice((int)offset, 4), entry.Item1 ); + MemoryMarshal.Write( span.Slice((int)(offset + 4), 4), (uint)(locbase + entry.Item2) ); + MemoryMarshal.Write( span.Slice((int)(offset + 8), 4), entry.Item3 ); + } + + // string table + CollectionsMarshal.AsSpan( strlist ).CopyTo( span.Slice((int)strbase, strlist.Count ) ); + // idxes table + MemoryMarshal.AsBytes( CollectionsMarshal.AsSpan(idxlist) ) + .CopyTo( span.Slice((int)idxbase, idxlist.Count * 2) ); + // inst table + MemoryMarshal.AsBytes( CollectionsMarshal.AsSpan(cmdlist) ) + .CopyTo( span.Slice((int)cmdbase, cmdlist.Count * 4) ); + + return result; + } + static List<(uint, Memory)> Parse( string data, string serial, string order ) { + var retval = new List<(uint, Memory)>(); + var state = ParserState.WaitLock; + var token = new List(); + var line = 1; + var locks = new List(); + Lock? currlock = null; + LockHole? currhole = null; + + Func token_str = () => { + var r = new string( CollectionsMarshal.AsSpan(token) ); + token.Clear(); + return r; + }; + + Action error = (string msg) => { + Console.WriteLine($"Error<{msg}> at line {line}: {msg}"); + Environment.Exit(1); + }; + + Action incr = (char ch) => { + switch(ch) { + case '[': { + if(state == ParserState.WaitLock || state == ParserState.ReadHole || state == ParserState.NeedPart ){ + state = ParserState.ReadLock; + if( currlock != null ) { + locks.Add( currlock.Value ); + } + currlock = new Lock(); + } + else if(state == ParserState.WaitPart) { } + else error($"error occur [ on {state}"); + } + break; + case ']': { + if(state == ParserState.ReadLock) { + state = ParserState.WaitHole; + var name = token_str(); + if(name.ToLower() == "gameinfo") { + retval.Add( Lade(locks, serial, order) ); + state = ParserState.WaitPart; + locks = new List(); + currhole = null; + currlock = null; + } + else if( name.Length > 0 ) { + currlock = new Lock { + name = name, + holes = new List() + }; + 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}"); + } + break; + case '\r': case '\n': { + if(state == ParserState.WaitHole) { + state = ParserState.ReadHole; + } + else if( state == ParserState.ReadKey){ + var token = token_str(); + var pass = false; + if( currhole != null ){ + var (name, keys) = currhole.Value; + if( token.Length > 0 ){ + keys.Last().Add(token); + } + + if( PASSHOLE.Contains(name.ToLower()) ){ + pass = true; + } + } + if( !pass ){ + if( currlock != null ){ + if( currhole == null ) error("不应该来到这里"); + currlock.Value.holes.Add( currhole.Value ); + } + currhole = null; + } + state = ParserState.ReadHole; + } + else if(state == ParserState.ReadLock){ + error($"error occur newline on {state}"); + } + else if(state == ParserState.ReadHole){ + if(token.Count > 0 && token.Find(c => c != ' ' && c != '\t') != 0 ){ + error($"error occur newline on {state}"); + } + else token.Clear(); + } + else if(state == ParserState.WaitPart){ + state = ParserState.NeedPart; + } + else {} + if( ch == '\n' ) ++line; + } + break; + case '=': { + if(state == ParserState.ReadHole){ + state = ParserState.ReadKey; + currhole = (token_str(), new List>{ new List() }); + } + 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}"); + } + break; + case ',': { + if( state == ParserState.ReadKey ){ + if( token.Count > 0 ){ + if( currhole == null ) error("不应该来到这里"); + var token = token_str(); + var (name, keys) = currhole.Value; + keys.Last().Add(token); + } + } + else if( state == ParserState.ReadHole ){ + var (name, cmd) = currlock.Value.holes.Last(); + currlock.Value.holes.RemoveAt(currlock.Value.holes.Count-1); + if( token.Count > 0 ) cmd.Last().Add(token_str()); + currhole = (name, cmd); + 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}"); + } + break; + case ';': { + if( state == ParserState.ReadKey){ + if(token.Count > 0) currhole.Value.Item2.Last().Add(token_str()); + currhole.Value.Item2.Add(new List()); + } + else if( state == ParserState.ReadLock){ + token.Add( ch ); + } + else if( state == ParserState.ReadHole){ + var (name, cmd) = currlock.Value.holes.Last(); + currlock.Value.holes.RemoveAt(currlock.Value.holes.Count-1); + if( token.Count > 0 ) cmd.Last().Add(token_str()); + cmd.Add(new List()); + currhole = (name, cmd); + state = ParserState.ReadKey; + } + else if( state == ParserState.WaitPart) {} + else if( state == ParserState.NeedPart ) { + state = ParserState.WaitPart; + } + else error($"error occur ; on {state}"); + } + break; + case ' ': { + if( state == ParserState.ReadLock || state == ParserState.ReadHole ){ + token.Add( ch ); + } + else if( state == ParserState.NeedPart ) { + state = ParserState.WaitPart; + } + else {} + } + break; + default: { + if( state == ParserState.ReadLock || state == ParserState.ReadHole ){ + token.Add( ch ); + } + else if( state == ParserState.ReadKey ){ + if( "0123456789abcdefABCDEF".Contains(ch) ){ + token.Add( ch ); + } + else if(currhole != null) { + if(currhole.Value.Item1 == "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}"); + } + break; + } + }; + + Action done = () => { + if (state != ParserState.WaitPart && state != ParserState.NeedPart) { + error($"Unexpected end of file {state} in {order}"); + } + }; + + foreach (char ch in data){ + incr(ch); + } + done(); + return retval; + } + static List<(string, List<(uint, Memory)>)> Transform( List<(string, string)> list ) { + var retval = new List<(string, List<(uint, Memory)>)>(); + foreach (var (order, serial) in list) { + var file = $"./gba/{order}.u8"; + try{ + var chtdata = File.ReadAllText(file); + if (chtdata.Length > 0) { + var cheats = Parse( chtdata, serial, order ); + retval.Add((serial, cheats)); + } + else { + //Console.WriteLine($"{order} is empty"); + retval.Add((serial, new List<(uint, Memory)>())); + } + } + catch( Exception e ){ + Console.WriteLine($"{e} not found"); + retval.Add((serial, new List<(uint, Memory)>())); + } + } + return retval; + } + static List<(string, string)> LoadList() { + var retval = new List<(string, string)>(); + var file_content = File.ReadAllText("serial.json"); + + using (JsonDocument doc = JsonDocument.Parse(file_content)) + { + JsonElement root = doc.RootElement; + + HashSet set = new HashSet(); + foreach (var item in root.EnumerateObject()) + { + string? serial = item.Value.GetString(); + if( set.Contains(item.Name) ) Console.WriteLine($"重复 {item.Name}"); + else if (serial != null && serial != "????") { + set.Add( item.Name ); + retval.Add(( item.Name, serial )); + } + } + } + + return retval; + } + + static byte[] Format( List<(string, List<(uint, Module)>)> cheats ) { + cheats.Sort( ((string, List<(uint, Module)>) a, (string, List<(uint, Module)>) b) => { + return a.Item1.CompareTo(b.Item1); + } ); + Console.WriteLine($"valid rom has {cheats.Count}"); + + List sers; + List offs; + uint chtc; + uint maxl; + { + (List, List, uint, uint, string) ret = (new List(), new List(), 0, 0, ""); + uint last = 0; + foreach(var (serial, cheat) in cheats){ + var val = serial.Substring(0, 3); + if( ret.Item5 != val ){ + if( ret.Item2.Count > 0 && ret.Item3 - last > ret.Item4 ){ + ret.Item4 = ret.Item3 - last; + } + ret.Item1.AddRange( Encoding.ASCII.GetBytes(val) ); + ret.Item2.Add((ushort)ret.Item3); + last = ret.Item3; + ret.Item5 = val; + } + ret.Item3 += (uint)cheat.Count; + } + if( ret.Item2.Count > 0 ){ + if( ret.Item3 - last > ret.Item4 ){ + ret.Item4 = ret.Item3 - last; + } + ret.Item2.Add((ushort)ret.Item3); + } + sers = ret.Item1; + offs = ret.Item2; + chtc = ret.Item3; + maxl = ret.Item4; + } + + List chts; + List expanded; + { + (List, List, uint) ret = (new List(), new List(), Align((uint)(8 + sers.Count + offs.Count*2 + chtc * 8), 32)); + foreach(var (serial, cheat) in cheats){ + var val = serial[3]; + foreach(var (id, bin) in cheat) { + var off = ret.Item3 + ret.Item2.Count; + ret.Item1.Add((uint)((val) | (off<<3))); + ret.Item1.Add( id ); + ret.Item2.EnsureCapacity( ret.Item2.Count + bin.Length ); + ret.Item2.AddRange( MemoryMarshal.ToEnumerable(bin) ); + } + } + chts = ret.Item1; + expanded = ret.Item2; + } + Console.WriteLine($"name: {sers.Count} cheats: {chtc} maxl: {maxl}"); + + var serialbase = 8; + var offsetbase = serialbase + sers.Count; + var cheatbase = offsetbase + offs.Count * 2; + var expandbase = Align((uint)(cheatbase + chts.Count*4), 32 ); + var total = expandbase + expanded.Count; + + var retval = new byte[total]; + var output = new Memory(retval); + var span = output.Span; + span[0] = (byte)'A'; + span[1] = (byte)'C'; + span[2] = (byte)'L'; + span[3] = 1; + + MemoryMarshal.Write(span.Slice(4, 2), (ushort)(sers.Count / 3)); + MemoryMarshal.Write(span.Slice(6, 2), (ushort)maxl); + + CollectionsMarshal.AsSpan( sers ).CopyTo( span.Slice(serialbase, sers.Count) ); + MemoryMarshal.AsBytes( CollectionsMarshal.AsSpan(offs) ) + .CopyTo( span.Slice(offsetbase, cheatbase - offsetbase) ); + MemoryMarshal.AsBytes( CollectionsMarshal.AsSpan(chts) ) + .CopyTo( span.Slice(cheatbase, chts.Count * 4) ); + CollectionsMarshal.AsSpan( expanded ).CopyTo( span.Slice((int)expandbase, expanded.Count) ); + + return retval; + } + + static void Main(string[] args) { + var list = LoadList(); + var roms = Transform(list); + Console.WriteLine($"all rom has {roms.Count}"); + var idx = new Dictionary>(); + foreach( var (serial, cheat) in roms ){ + if(cheat.Count == 0 ){ + continue; + } + + if( !idx.ContainsKey(serial) ){ + idx[serial] = new List<(uint, Module)>(); + } + idx[serial].AddRange( cheat ); + } + var content = Format( idx.Select(g => (g.Key, g.Value)).ToList() ); + File.WriteAllBytes("gba.acl", content); + } +} \ No newline at end of file